【问题标题】:Recursive ConcurrentHashMap.computeIfAbsent() call never terminates. Bug or "feature"?递归 ConcurrentHashMap.computeIfAbsent() 调用永远不会终止。错误还是“功能”?
【发布时间】:2015-05-04 14:10:48
【问题描述】:

前段时间,I've blogged about a Java 8 functional way of calculating fibonacci numbers recursively,带有ConcurrentHashMap 缓存和新的有用的computeIfAbsent() 方法:

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class Test {
    static Map<Integer, Integer> cache = new ConcurrentHashMap<>();

    public static void main(String[] args) {
        System.out.println(
            "f(" + 8 + ") = " + fibonacci(8));
    }

    static int fibonacci(int i) {
        if (i == 0)
            return i;

        if (i == 1)
            return 1;

        return cache.computeIfAbsent(i, (key) -> {
            System.out.println(
                "Slow calculation of " + key);

            return fibonacci(i - 2) + fibonacci(i - 1);
        });
    }
}

我选择ConcurrentHashMap 是因为我正在考虑通过引入并行性(我最终没有这样做)来使这个示例更加复杂。

现在,让我们将数字从8 增加到25 并观察会发生什么:

        System.out.println(
            "f(" + 25 + ") = " + fibonacci(25));

程序永远不会停止。在方法内部,有一个永远运行的循环:

for (Node<K,V>[] tab = table;;) {
    // ...
}

我正在使用:

C:\Users\Lukas>java -version
java version "1.8.0_40-ea"
Java(TM) SE Runtime Environment (build 1.8.0_40-ea-b23)
Java HotSpot(TM) 64-Bit Server VM (build 25.40-b25, mixed mode)

Matthias, a reader of that blog post also confirmed the issue (he actually found it).

这很奇怪。我会期待以下两种中的任何一种:

  • 有效
  • 它会抛出一个ConcurrentModificationException

但只是从不停止?这似乎很危险。它是一个错误吗?还是我误解了某些合同?

【问题讨论】:

    标签: java recursion java-8 concurrenthashmap


    【解决方案1】:

    这当然是一个“功能”ConcurrentHashMap.computeIfAbsent() Javadoc 内容如下:

    如果指定的键尚未与值关联,则尝试使用给定的映射函数计算其值并将其输入到此映射中,除非为空。整个方法调用以原子方式执行,因此每个键最多应用一次该函数。在计算过程中,其他线程对该映射的某些尝试更新操作可能会被阻止,因此计算应该简短且简单,不得尝试更新此映射的任何其他映射

    “不得” 措辞是一个明确的约定,我的算法违反了它,尽管不是出于相同的并发原因。

    有趣的是没有ConcurrentModificationException。相反,程序永远不会停止 - 在我看来这仍然是一个相当危险的错误(即infinite loops. or: anything that can possibly go wrong, does)。

    注意:

    HashMap.computeIfAbsent()Map.computeIfAbsent() Javadoc 不禁止这种递归计算,这当然是荒谬的,因为缓存的类型是Map&lt;Integer, Integer&gt;,而不是ConcurrentHashMap&lt;Integer, Integer&gt;。子类型彻底重新定义超类型合同是非常危险的(Set vs. SortedSet 是问候)。 因此也应该禁止在超类型中执行这种递归。

    【讨论】:

    • 很好的发现。我建议针对 JDK 提交错误报告/RFE。
    • 完成了,让我们看看它是否被接受...如果是,我会更新一个链接。
    • 在我看来,ConcurrentHashMap 中不允许对其他映射进行这种类型的递归修改,因为它的合同的另一个重要部分是:“整个方法调用是原子执行的,因此每个键最多应用该函数一次。" 您的程序很可能通过违反“无递归修改”合同,试图获取它已经持有的锁,并与自身陷入死锁,没有在无限循环中运行。
    • 来自 JavaDoc:IllegalStateException - if the computation detectably attempts a recursive update to this map that would otherwise never complete
    • @LukasEder 即使使用HashMap 也不行。 bugs.openjdk.java.net/browse/JDK-8172951
    【解决方案2】:

    这已在JDK-8062841 中修复。

    2011 proposal 中,我在代码审查期间发现了这个问题。 JavaDoc 已更新并添加了临时修复。由于性能问题,它在进一步的重写中被删除。

    2014 discussion 中,我们探索了更好地检测和失败的方法。请注意,为了考虑低级别的更改,一些讨论已离线发送到私人电子邮件。虽然不能涵盖所有情况,但常见情况不会活锁。这些 fixes 在 Doug 的存储库中,但尚未进入 JDK 版本。

    【讨论】:

    • 很有趣,谢谢分享!我的报告也被接受为JDK-8074374
    • @Ben 对不起,我真的很惊讶你的 SO 评分有多低,尽管在知识库中对无锁(并且接近无锁)这样复杂的事情做出了如此重大的贡献并发。
    • @SashaSalauyou 谢谢。我经常在 cmets 中提供快速答案,而不是花时间在更长、更完整的答案上。不过,评论分数不会增加声誉。不过,我可能对追求代表的兴趣低于平均水平。
    • @Ben 不太固定:boom
    • @ScottMcKinney 不错!看起来相同的断言可以应用于transfer,这是一个遗漏的案例。你能发邮件给concurrency-interest@cs.oswego.edu 引起Doug 的注意吗?
    【解决方案3】:

    这与错误非常相似。 因为,如果您创建容量为 32 的缓存,您的程序将一直运行到 49。 有趣的是,参数 sizeCtl =32 + (32 >>> 1) + 1) =49! 可能是调整大小的原因?

    【讨论】:

    • 我没有时间去挖掘源代码,但我认为你是对的。我们在生产中不断地做到这一点。一旦我们将CHM 初始容量增加到一个非常高的数字,这种情况就消失了。在我们可以重构我们的代码之前,这就是我们要做的......
    猜你喜欢
    • 1970-01-01
    • 2014-05-13
    • 1970-01-01
    • 2021-09-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-08
    相关资源
    最近更新 更多