5

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

4 回答 4

16

您正在使用一个众所周知的非线程安全类并抱怨死锁。我看不出这里有什么问题。

还有,怎么样ExecutionService

strangely lacking

?

一个常见的误解是,使用例如aHashMap最多只能得到一些陈旧的数据。查看一个关于如何通过这样做来炸毁 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永远不会返回并且程序挂起。但这既不是死锁,也不是活锁,而是竞态条件

于 2009-07-01T09:33:56.200 回答
2

你怎么理解死锁?

代码至少有两个问题。同时HashMap从多个线程中使用,因此可以进入无限循环。您正在迭代条目集,同时可能会更改底层数据结构(即使每个单独的操作都是同步的hasNext/next不会是原子的)。

另请注意,使用最新同步安全版本 (SSR) 的 1.6.0 版本是 1.6.0_13 和 1.6.0_14。

于 2009-07-01T09:34:59.630 回答
1

我相信您的地图正在同时修改。如果在您的迭代操作正在进行时调用 put(),在某些情况下(特别是如果发生调整大小),您可能会陷入无限循环。这是一个众所周知的行为(请参见此处)。

死锁和无限循环的表现会非常不同。如果你有一个真正的死锁,线程转储将清楚地显示互锁线程。另一方面,一旦你进入一个无限循环,你的 CPU 将飙升,并且每次转储时堆栈跟踪都会有所不同。

这与 Executor 无关,而与HashMap 的不安全并发使用有关,而 HashMap从未被设计为以这种方式使用。事实上,用一组线程很容易重现这个问题。

最好的解决方案是切换到 ConcurrentHashMap。如果切换到同步的 HashMap 或 Hashtable,则不会陷入无限循环,但在迭代过程中仍可能出现 ConcurrentModificationExceptions。

于 2009-07-17T06:13:29.770 回答
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... 
于 2009-07-01T10:35:32.270 回答