Recursive ConcurrentHashMap.computeIfAbsent() 调用永远不会终止。错误还是"feature"?



不久前,我在博客中介绍了一种Java 8函数递归计算斐波那契数的方法,该方法使用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:UsersLukas>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也证实了这个问题(他确实发现了)。

这很奇怪。我本以为会出现以下两种情况中的任何一种:

  • 它有效
  • 它抛出一个ConcurrentModificationException

但就是永不停歇?这似乎很危险。是虫子吗?或者我误解了一些合同?

这当然是一个"功能"ConcurrentHashMap.computeIfAbsent() Javadoc显示:

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

"决不能"的措辞是一个明确的约定,我的算法违反了这一约定,尽管不是出于同样的并发原因。

仍然有趣的是没有ConcurrentModificationException。相反,程序从未停止——在我看来,这仍然是一个相当危险的错误(即无限循环。或者:任何可能出错的东西都会出错)。

注:

HashMap.computeIfAbsent()Map.computeIfAbsent()Javadoc不禁止这种递归计算,这当然很荒谬,因为缓存的类型是Map<Integer, Integer>,而不是ConcurrentHashMap<Integer, Integer>。对于子类型来说,彻底重新定义超类型契约是非常危险的(SetSortedSet是问候语)因此,在超类型中也应该禁止执行这种递归

这在JDK-8062841中得到了修复。

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

在2014年的讨论中,我们探讨了如何更好地检测和失败。请注意,一些讨论被离线到私人电子邮件中,以考虑低级别的更改。虽然不是每一个案例都能涵盖,但普通案例不会被锁定。这些修复程序在Doug的存储库中,但尚未发布到JDK中。

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

相关内容

  • 没有找到相关文章

最新更新