Java线程意外行为

时间:2017-01-31 15:37:12

标签: java multithreading

我们一直在研究线程错误,但不确定这是怎么回事。以下是我们代码中的最小化示例。有一个缓存保存从数据库中检索的数据(或者:“一个冗长的同步操作”,就这个例子而言)。有一个用于重新加载缓存的线程,而其他线程尝试查询缓存。有一段时间缓存为空,等待重新加载。在此期间它不应该是可查询的,我们试图通过同步访问缓存的方法来强制执行此操作 - 包括读取和写入。然而,如果你运行这个课程一段时间,你将获得search()中的NPE。这怎么可能?

Java文档声明“对同一对象的两个同步方法的调用不可能进行交错。当一个线程正在为对象执行同步方法时,所有其他线程为同一个对象块调用同步方法(暂停执行)直到第一个线程完成对象“。

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class CacheMultithreading01 {
    private long dt = 1000L;

    public static void main(String[] args) {
        CacheMultithreading01 cm = new CacheMultithreading01();
        cm.demonstrateProblem();
    }

    void demonstrateProblem() {
        QueryableCache cache = new QueryableCache();
        runInLoop("Reload", new Runnable() {
            @Override
            public void run() {
                cache.reload();
            }
        });
        runInLoop("Search", new Runnable() {
            @Override
            public void run() {
                cache.search(2);
            }
        });
        // If the third "runInLoop" is commented out, no NPEs
        runInLoop("_Clear", new Runnable() {
            @Override
            public void run() {
                cache.clear();
            }
        });
    }

    void runInLoop(String threadName, Runnable r) {
        new Thread(new Runnable() {
            @Override
            public synchronized void run() {
                while (true) {
                    try {
                        r.run();
                    } catch (Exception e) {
                        log("Error");
                        e.printStackTrace();
                    }
                }
            }
        }, threadName).start();
    }

    void log(String s) {
        System.out.format("%d %s %s\n", System.currentTimeMillis(), Thread
                .currentThread().getName(), s);
    }

    class QueryableCache {
        private List<Integer> cache = new ArrayList<>();

        public synchronized void reload() {
            clear();
            slowOp(); // simulate retrieval from database
            cache = new ArrayList<>(Arrays.asList(1, 2, 3));
        }

        public synchronized void clear() {
            cache = null;
        }

        public synchronized Integer search(Integer element) {
            if (cache.contains(element))
                return element;
            else
                return null;
        }

        private void slowOp() {
            try {
                Thread.sleep(dt);
            } catch (InterruptedException e) {
            }
        }
    }
}
//java.lang.NullPointerException
//at examples.multithreading.cache.CacheMultithreading01$QueryableCache.search(CacheMultithreading01.java:73)
//at examples.multithreading.cache.CacheMultithreading01$2.run(CacheMultithreading01.java:26)
//at examples.multithreading.cache.CacheMultithreading01$4.run(CacheMultithreading01.java:44)
//at java.lang.Thread.run(Thread.java:745)

我们不明白为什么即使代码同步也会发生NPE。我们也不明白为什么如果我们将runInLoop(执行cache.clear的那个)的第三次调用注释掉,NPE就会停止发生。 我们还尝试使用ReentrantReadWriteLock实现锁定 - 结果是相同的。

5 个答案:

答案 0 :(得分:3)

如果search为空,则必须检查cache方法。否则,contains中的search上的NullPointerException会在您cache - 方法中将clear设置为空的情况下抛出p

答案 1 :(得分:3)

由于您没有更高级的锁定功能,因此您可以连续拨打clear()search()。这显然会导致NPE。

调用reload()search()不会导致问题,因为在重新加载时,缓存会在同步块内被清除,然后重建,从而阻止在其间执行其他(搜索)操作。 / p>

为什么clear()方法会使cache处于“错误”状态(search()甚至不检查)?

答案 2 :(得分:2)

&#34;有一段时间缓存为空,等待重新加载。&#34; 这是你的问题:clear将东西设置为null,然后返回,释放同步锁,允许其他人访问。 制作&#34; new&#34;会更好。赋值原子而不是clear()

假设slowOp()需要返回缓存的数据(private List<Integer> slowOp()),则在分配之前检索该数据

ArrayList<Integer> waitingForData = slowOp(); cache = watingForData; 这个&#39;更新&#39;仅在数据可用后才缓存。分配是一种原子操作,在更新引用时,没有任何东西可以访问缓存

答案 3 :(得分:2)

同步正常工作。

问题是方法[{'c_price_from': 15690, 'd_end': '2017-04-16', 'd_start': '2017-04-09', 'nd_price_discount': 27, 'nl_hotel_id': [24810], 'nl_term_id': 93902083, 'nl_tour_id': 839597}, {'c_price_from': 27371, 'd_end': '2017-04-17', 'd_start': '2017-04-12', 'nd_price_discount': 4, 'nl_hotel_id': [49316], 'nl_term_id': 100804770, 'nl_tour_id': 1017864}, {'c_price_from': 32175, 'd_end': '2017-04-17', 'd_start': '2017-04-12', 'nd_price_discount': 4, 'nl_hotel_id': [49316], 'nl_term_id': 100800962, 'nl_tour_id': 1017864}, 将缓存放到clear。并且无法保证在null之前调用reload方法。

另外,请注意方法search,它没有释放锁定。因此,当您等待reload完成时,其他方法无法执行。

答案 4 :(得分:1)

三个不同的线程在没有明确交错的情况下调用缓存的clear()search()和reload()。由于交错并不保证为clear()和search()线程获得锁定序列,因此搜索线程可能有机会在clear()线程之后获取对象的锁定。在这种情况下,搜索将导致NullPointerException。

您可能必须在搜索对象中检查等于null的缓存,并且可以在search()方法中执行reload()。这将保证搜索结果或在适用时返回null。