执行HashMap操作时为什么ExecutorService会死锁?

时间:2009-07-01 09:27:32

标签: java concurrency hashmap

运行以下类时,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():

  • 我可以在我的Core2 Duo 2.4Ghz笔记本电脑上重现这个问题,但在调试中运行时不能重现
  • 我可以在我的Core2 Quad上进行调试,但我把它挂在了RDP上,所以无法获得 明天的堆栈跟踪

下面的大多数答案都是关于HashMap的非线程安全性,但我在HashMap中找不到锁定的线程 - 它全部在ExecutionService代码中(和Unsafe.park())。我明天会密切研究这些主题。

所有这一切都是因为自定义抽象Map实现不是线程安全的,所以我着手确保所有实现都是线程安全的。本质上,我想确保我对ConcurrentHashMap等的理解正是我所期望的,但却发现ExecutionService奇怪地缺乏......

4 个答案:

答案 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...