recursionConcurrentHashMap.computeIfAbsent()调用永不终止。 错误或“function”?

前些时候, 我已经写了一个Java 8函数recursion计算 computeIfAbsent() 数字的方法 ,用一个ConcurrentHashMapcaching和新的有用的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); }); } } 

我select了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也证实了这个问题(他确实发现了这个问题) 。

这很奇怪 我会期待以下任何一个:

  • 有用
  • 它抛出一个ConcurrentModificationExceptionexception

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

这在JDK-8062841中得到解决 。

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

在2014年的讨论中 ,我们探索了更好的检测和失败的方法。 请注意,有些讨论是通过私人电子邮件进行的,以考虑低层次的变更。 虽然不是每个案件都可以被覆盖,但是普通案件不会活着。 这些修复程序位于Doug的存储库中,但尚未将其修改为JDK发行版。

这当然是一个“function”ConcurrentHashMap.computeIfAbsent() Javadoc读取:

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

“不得”的措辞是一个明确的合同,我的algorithm违反,虽然不是出于同样的并发原因。

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

注意:

Map.computeIfAbsent() HashMap.computeIfAbsent()Map.computeIfAbsent() Javadoc不禁止这样的recursion计算,这当然是荒谬的,因为caching的types是Map<Integer, Integer> ,而不是ConcurrentHashMap<Integer, Integer> 。 子types急剧重新定义超types合约是非常危险的( SetSortedSet是问候语)。 因此,它也应该被禁止在超types中执行这种recursion。

这个bug非常相似。 因为,如果你创build了容量为32的caching,你的程序将会工作到49.有趣的是,参数sizeCtl = 32 +(32 >>> 1)+ 1)= 49! 可能是resize的原因?