有时我们会提供糟糕的建议。 就像该文章中有关如何将Java 8用于缓存的功能性方法来计算斐波那契数的文章一样 。 正如我们的读者之一马蒂亚斯(Matthias)在评论中注意到的那样 ,提出的算法可能永远不会停止。 考虑以下程序:
public class Test {static Map<Integer, Integer> cache = new ConcurrentHashMap<>();public static void main(String[] args) {System.out.println("f(" + 25 + ") = " + fibonacci(25));}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);});}
}
它将至少在以下Java版本上无限期运行:
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)
这当然是“功能” 。 ConcurrentHashMap.computeIfAbsent()
Javadoc读取:
如果指定的键尚未与某个值关联,则尝试使用给定的映射函数计算其值,除非为null,否则将其输入此映射。 整个方法调用是原子执行的,因此每个键最多可应用一次该功能。 在进行计算时,可能会阻止其他线程对此映射进行的某些尝试的更新操作,因此计算应简短而简单, 并且不得尝试更新此映射的任何其他映射 。
尽管并非出于相同的并发原因,“不得”的措辞是明确的合同,我的算法违反了该合同。
Javadoc还读取:
抛出:
IllegalStateException-如果计算可检测到尝试对此地图进行递归更新,否则将永远无法完成
但是不会抛出该异常。 也没有任何ConcurrentModificationException。 相反,该程序永远不会停止。
解决此具体问题的最简单的使用现场解决方案是不使用ConcurrentHashMap,而仅使用HashMap:
static Map<Integer, Integer> cache = new HashMap<>();
覆盖超类型合约的子类型
Map.computeIfAbsent()
HashMap.computeIfAbsent()
或Map.computeIfAbsent()
Javadoc禁止这种递归计算,这当然是荒谬的,因为缓存的类型是Map<Integer, Integer>
,而不是ConcurrentHashMap<Integer, Integer>
。 子类型彻底重新定义超级类型协定是非常危险的( Set
vs. SortedSet
是问候)。 因此,在超级类型中也应禁止执行此类递归。
进一步参考
尽管合同问题只是人们的看法,但停顿问题显然是一个漏洞。 我还在Stack Overflow上记录了此问题,在该问题中 , Ben Manes提供了一个有趣的答案,导致了先前的错误报告(截至2015年初尚未解决):
- https://bugs.openjdk.java.net/browse/JDK-8062841
我自己的报告(可能是上述报告的副本)也很快被接受,原因是:
- https://bugs.openjdk.java.net/browse/JDK-8074374
Oracle正在研究此问题时,请记住:
切勿在
ConcurrentHashMap.computeIfAbsent()
方法内部进行递归。 如果您正在实现集合,并且认为编写一个可能无限的循环是个好主意,请再考虑一下,然后阅读我们的文章:无限循环。 或者:可能出错的任何东西都可以 )
墨菲总是对的。
翻译自: https://www.javacodegeeks.com/2015/03/avoid-recursion-in-concurrenthashmap-computeifabsent.html