为什么执行HashMap的操作时,不会ExecutorService的僵局?(Why does Ex

2019-09-17 09:02发布

当运行以下类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);
}

}

那么,为什么会出现这种情况? 或者更好的是 - 我怎么能写一个测试,以确保一个自定义的抽象地图的实现是线程安全的吗? (有些实现有多个地图,另一种委托给高速缓存执行等)

一些背景:发生这种情况的Java 1.6.0_04和1.6.0_07在Windows上。 我知道,这个问题是来自sun.misc.Unsafe.park():

  • 我可以重现这个问题上我的酷睿双核2.4GHz的笔记本电脑,但不能同时运行调试
  • 我可以在工作中对我的酷睿2四核调试,但我已经把它挂在RDP,所以将无法得到一个堆栈跟踪到明天

下面大多数的答案是关于HashMap中的非线程安全的,但我能找到的HashMap没有锁定线程 - 这是所有在ExecutionService代码(和Unsafe.park())。 我会仔细检查线程明天。

这一切都是因为一个自定义的抽象地图实现不是线程安全的,所以我开始着手确保所有的实现是线程安全的。 从本质上说,我想确保我的ConcurrentHashMap等的理解是正是我期待的,但已经找到了ExecutionService是奇怪的缺乏......

Answer 1:

您使用的是著名的不是线程安全类和抱怨死锁。 我看不出这个问题是在这里。

此外,如何是ExecutionService

strangely lacking

这是一个常见的误解,通过使用 HashMap ,你将最多获得一些陈旧的数据。 见一个漂亮的竞争条件你怎么能这样做只是炸毁你的JVM。

了解为什么发生这种情况是一个非常棘手的过程,需要JVM和类库的内部的知识。

对于ConcurrentHashMap的,只读过的javadoc -它应该澄清你的疑问。 如果没有,看看在实践Java并发 。


更新:

我设法复制您的情况,但它不是一个死锁。 其中的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永远不会返回和程序挂起。 但它既不是僵局,也不是活锁,而是一个竞争条件



Answer 2:

你说的僵局明白了吗?

至少有两个问题的代码。 该HashMap从多个线程同时等使用可以进入一个无限循环。 你迭代的设定,同时潜在地改变底层数据结构(即使每个单独的操作是同步的条目hasNext / next不会原子)。

还要指出的是1.6.0最新与最新Synhronized安全发布(SSR)的版本是1.6.0_13和1.6.0_14。



Answer 3:

我相信你的地图被同时修改。 如果把()被调用,而你的迭代操作过程中,在一定条件下(特别是如果大小调整情况),你可能会在一个无限循环结束。 这是一个相当知名的行为(见这里 )。

死锁和无限循环会表现出自己非常不同。 如果你有一个真正的死锁,线程转储将清楚地显示联锁线程。 在另一方面,一旦你陷入一个死循环,你的CPU将秒杀高和堆栈跟踪会在您每次乘坐转储时间而变化。

这有什么好做的执行人及一切与不安全的同时使用HashMap的产品,其设计初衷不使用这种方式。 其实,这是很容易重现此问题与线程少数的数组。

这个最好的解决办法是切换到的ConcurrentHashMap。 如果切换到同步的HashMap或Hashtable的,你不会进入一个无限循环,但你仍然可以迭代过程中得到ConcurrentModificationExceptions。



Answer 4:

在进行测试工作方面 - 而不是:

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


文章来源: Why does ExecutorService deadlock when performing HashMap operations?