递归ConcurrentHashMap.computeIfAbsent()调用永远不会终止。错误或"功能"?

时间:2015-03-03 19:14:36

标签: java recursion java-8 concurrenthashmap

前一段时间,I've blogged about a Java 8 functional way of calculating fibonacci numbers recursivelyConcurrentHashMap缓存和新的有用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

但是永远不要停止?这似乎很危险。这是一个错误吗?还是我误解了一些合同?

3 个答案:

答案 0 :(得分:52)

这当然是&#34;功能&#34; ConcurrentHashMap.computeIfAbsent() Javadoc上写着:

  

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

&#34;&#34; 措辞是一个明确的合同,我的算法违反了这个合同,尽管不是出于同样的并发原因。

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

注意:

HashMap.computeIfAbsent()Map.computeIfAbsent() Javadoc禁止这样的递归计算,这当然是荒谬的,因为缓存的类型是Map<Integer, Integer>,而不是ConcurrentHashMap<Integer, Integer> 。子类型极大地重新定义超类型合同是非常危险的(SetSortedSet是问候语)。 因此,在超级类型中也应禁止执行此类递归。

答案 1 :(得分:48)

这已在JDK-8062841中修复。

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

2014 discussion中,我们探索了更好地检测和失败的方法。请注意,部分讨论是针对私人电子邮件进行的,以便考虑低级别的更改。虽然不是每个案例都可以涵盖,但常见案例不会活锁。这些fixes位于Doug的存储库中,但尚未成为JDK版本。

答案 2 :(得分:4)

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