Java线程意外行为



我们已经研究了一段时间的线程错误,不确定这是怎么可能的。下面是我们代码中的一个最小化示例。有一个缓存保存从数据库检索的数据(或者:就本例而言,是"一个漫长的同步操作")。有一个线程用于重新加载缓存,而其他线程则试图查询缓存。有一段时间缓存为空,等待重新加载。在这段时间内,它不应该是可查询的,我们试图通过同步访问缓存的方法来强制执行这一点,包括读取和写入。然而,如果你运行这个类一段时间,你会在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 %sn", 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-实现锁定,结果是一样的。

由于没有任何更高级的锁定,因此可以连续调用clear()search()。这显然会导致NPE。

调用reload()search()不会引起问题,因为在重新加载时,缓存会在同步块内被清除,然后重建,从而防止在两者之间执行其他(搜索)操作。

为什么有一个clear()方法会使cache处于"坏"状态(search()甚至不检查)?

如果cache为null,则必须签入search方法。否则,如果您之前在clear-方法中将cache设置为null,那么在search中对其调用contains可能会抛出NullPointerException

同步工作正常。

问题是方法clear将高速缓存放入null。并且不能保证reload方法会在search之前被调用。

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

"有一段时间缓存为null,等待重新加载。"这就是您的问题:clear将东西设置为null,然后返回,释放同步锁,允许其他人访问。最好使"新"分配成为原子,而不是clear()

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

ArrayList<Integer> waitingForData = slowOp(); cache = watingForData; 只有在数据可用后,才会"更新"缓存。赋值是一个原子操作,在更新引用时,任何东西都不能访问缓存

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

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

最新更新