运行以下类时,ExecutionService通常会死锁。
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorTest {
public static void main(final String[] args) throws InterruptedException {
final ExecutorService executor = Executors.newFixedThreadPool(10);
final HashMap<Object, Object> map = new HashMap<Object, Object>();
final Collection<Callable<Object>> actions = new ArrayList<Callable<Object>>();
int i = 0;
while (i++ < 1000) {
final Object o = new Object();
actions.add(new Callable<Object>() {
public Object call() throws Exception {
map.put(o, o);
return null;
}
});
actions.add(new Callable<Object>() {
public Object call() throws Exception {
map.put(new Object(), o);
return null;
}
});
actions.add(new Callable<Object>() {
public Object call() throws Exception {
for (Iterator iterator = map.entrySet().iterator(); iterator.hasNext();) {
iterator.next();
}
return null;
}
});
}
executor.invokeAll(actions);
System.exit(0);
}
}
那么为什么会这样呢?或者更好 - 我如何编写测试以确保自定义抽象映射的实现是线程安全的? (某些实现具有多个映射,另一个实现具有缓存实现等)
一些背景: 这发生在Windows上的Java 1.6.0_04和1.6.0_07下。我知道问题来自sun.misc.Unsafe.park():
下面的大多数答案都是关于HashMap的非线程安全性,但我在HashMap中找不到锁定的线程 - 它全部在ExecutionService代码中(和Unsafe.park())。我明天会密切研究这些主题。
所有这一切都是因为自定义抽象Map实现不是线程安全的,所以我着手确保所有实现都是线程安全的。本质上,我想确保我对ConcurrentHashMap等的理解正是我所期望的,但却发现ExecutionService奇怪地缺乏......
答案 0 :(得分:16)
你正在使用一个众所周知的非线程安全类并抱怨死锁。我没有看到这里的问题。
另外,ExecutionService
strangely lacking
一种常见的误解是,通过使用例如 a HashMap
,您最多会得到一些陈旧的数据。请参阅a beautiful race condition,了解如何通过这样做来炸毁JVM。
理解为什么会发生这种情况是一个非常棘手的过程,需要了解JVM和类库的内部结构。
至于ConcurrentHashMap,只需阅读javadoc - 它应该澄清你的问题。如果没有,请查看Java Concurrency in Practice。
<强>更新强>
我设法重现你的情况,但这并不是一个僵局。其中一个actions
永远不会完成执行。堆栈跟踪是:
"pool-1-thread-3" prio=10 tid=0x08110000 nid=0x22f8 runnable [0x805b0000]
java.lang.Thread.State: RUNNABLE
at ExecutorTest$3.call(ExecutorTest.java:36)
at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:303)
at java.util.concurrent.FutureTask.run(FutureTask.java:138)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:619)
看起来我链接到的确切情况 - HashMap被调整大小,并且由于调整迭代器大小的内部机制被卡在无限循环中。
发生这种情况时,invokeAll
永远不会返回,程序会挂起。但它既不是僵局,也不是活锁,而是竞争条件。
答案 1 :(得分:2)
你对僵局有什么了解?
代码至少有两个问题。 HashMap
同时从多个线程使用,因此可以进入无限循环。您正在迭代条目集,同时可能会更改基础数据结构(即使每个单独的操作已同步hasNext
/ next
也不是原子的)。
另请注意,最新的Synhronized Security Release(SSR)1.6.0版本为1.6.0_13和1.6.0_14。
答案 2 :(得分:1)
我相信您的地图正在同时修改。如果在迭代操作正在进行时调用put(),在某些条件下(特别是如果调整大小发生),您可能最终处于无限循环中。这是一个众所周知的行为(见here)。
死锁和无限循环将以非常不同的方式呈现出来。如果你有一个真正的死锁,那么线程转储将清楚地显示互锁线程。另一方面,一旦进入无限循环,您的CPU将会高峰,并且每次进行转储时堆栈跟踪都会有所不同。
这与Executor无关,而且与不安全并发使用HashMap 无关,而这种方法从未被设计成以这种方式使用。事实上,使用一些线程来重现这个问题非常容易。
最佳解决方案是切换到ConcurrentHashMap。如果切换到同步的HashMap或Hashtable,则不会进入无限循环,但在迭代期间仍可能获得ConcurrentModificationExceptions。
答案 3 :(得分:0)
在进行测试工作方面 - 而不是:
executor.invokeAll(actions);
使用
executor.invokeAll(actions, 2, TimeUnit.SECONDS);
还要注意,要使测试真正起作用(并报告错误),您需要执行以下操作:
List<Future> results = executor.invokeAll(actions, 2, TimeUnit.SECONDS);
executor.shutdown();
for (Future result : results) {
result.get(); // This will report the exceptions encountered when executing the action ... the ConcurrentModificationException I wanted in this case (or CancellationException in the case of a time out)
}
//If we get here, the test is successful...