【问题标题】:Why does ExecutorService deadlock when performing HashMap operations?为什么ExecutorService在执行HashMap操作时会死锁?
【发布时间】:2009-07-01 09:27:32
【问题描述】:

在运行下面的类时,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 奇怪地缺乏......

【问题讨论】:

  • 我明天的当前操作: 1. 仔细检查在 HashMap [resize] 代码中停止的线程 2. 升级到最新的 vm
  • 在调整大小时不会停止任何线程 - 它们会调整大小并退出。检查线程卡在然后迭代 Callable。

标签: java concurrency hashmap


【解决方案1】:

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

另外,ExecutionService 怎么样

strangely lacking

?

这是一个常见的误解,即使用 例如 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 永远不会返回并且程序挂起。但这既不是死锁,也不是活锁,而是竞态条件

【讨论】:

  • 我明白了(我想 - 我会阅读有关比赛条件的信息)。问题是我发现没有线程实际上锁定在 HashMap 代码中。这一切都在 ExecutionService 排队功能中。例如Unsafe.park() 如前所述。
  • 我没有在使用 6u14 的 Core 2 上遇到死锁,所以对此我无话可说。您可能想尝试使用 ConcurrentHashMap 来查看死锁是否仍然存在。但我认为这里没有出现僵局的原因。
  • @Stephen - 我设法复制了它并添加了解释。
  • 不,问题根本不在于ExecutorService或ExecutorService的排队。问题是 HashMap 不是线程安全的,因此当从多个线程同时修改时,内部状态基本上是未定义的。在您的测试中,迭代器似乎陷入了无限循环。这就是问题所在。
【解决方案2】:

你是怎么理解死锁的?

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

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

【讨论】:

  • 您能否提供一个链接,详细了解 SSR 版本中包含的内容?
  • @pjp 它们现在被称为特殊 CPU(关键补丁更新,特别是因为它们目前与 Oracle quaterly CPU 不同步)。以下是撰写本文时带有风险矩阵的最新咨询的链接:oracle.com/technetwork/topics/security/… 通常不提供更多详细信息。
【解决方案3】:

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

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

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

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

【讨论】:

    【解决方案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... 
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-04-19
      • 1970-01-01
      • 1970-01-01
      • 2015-03-09
      • 2019-12-27
      • 2019-12-16
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多