Java 递归ConcurrentHashMap.ComputeFabSent()调用永远不会终止。错误或“错误”;“功能”;?

Java 递归ConcurrentHashMap.ComputeFabSent()调用永远不会终止。错误或“错误”;“功能”;?,java,recursion,java-8,concurrenthashmap,Java,Recursion,Java 8,Concurrenthashmap,不久前,使用ConcurrentHashMap缓存和新的、有用的ComputeFabSent()方法: import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class Test { static Map<Integer, Integer> cache = new ConcurrentHashMap<>(); public static void main

不久前,使用
ConcurrentHashMap
缓存和新的、有用的
ComputeFabSent()
方法:

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);
        });
    }
}
节目从不停止。在方法内部,有一个循环永远运行:

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

这很奇怪。我预计会出现以下两种情况:

  • 它起作用了
  • 它抛出一个
    ConcurrentModificationException
但就是不停?这似乎很危险。是虫子吗?还是我误解了一些合同?

这当然是一个“特征”。Javadoc的内容如下:

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

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

仍然有趣的是没有
ConcurrentModificationException
。相反,程序从不停止——在我看来,这仍然是一个相当危险的错误(即)

注: 或者Javadoc并不禁止这种递归计算,这当然是荒谬的,因为缓存的类型是
Map
,而不是
ConcurrentHashMap
。对于子类型来说,彻底重新定义超级类型契约是非常危险的(
Set
vs.
SortedSet
is)因此,在超类型中也应该禁止执行这种递归。

这在中是固定的

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


在本文中,我们探索了更好地检测和失败的方法。请注意,一些讨论被脱机发送到私人电子邮件,以考虑低级别的更改。虽然不是每个案例都可以涵盖,但普通案例不会被活锁。这些都在Doug的存储库中,但尚未发布到JDK版本中。

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

很好。我建议针对JDK提交一份bug报告/RFE。完成,让我们看看它是否被接受。。。如果是的话,我会用一个链接进行更新。在我看来,
ConcurrentHashMap
中不允许对其他映射进行这种递归修改,因为其契约的另一个重要部分是:“整个方法调用是以原子方式执行的,因此每个键最多应用一次函数。”很可能您的程序违反了“无递归修改”契约,试图获取它已经拥有的锁,并且自身处于死锁状态,未在无限循环中运行。从JavaDoc:
IllegalStateException-如果计算可检测到尝试对此映射进行递归更新,则即使使用
HashMap
也无法完成
@LukasEder,这是不正常的。非常有趣,谢谢分享!我的报告也被接受为@Ben sorry for offtop,但我真的很惊讶你的评价如此之低,尽管你在知识库中对诸如无锁(以及接近无锁)并发这样复杂的事情做出了如此重要的贡献。@SashaSalauyou谢谢。我经常在评论中提供一个快速的答案,而不是花时间在一个较长的、完整的答案上。不过,评论分数并不会提高声誉。不过,我可能对追逐rep的兴趣低于平均水平。@Ben还没有完全确定:@ScottMcKinney抓得好!看起来相同的断言可以应用于
传输
,这是一个遗漏的案例。你能同时发送电子邮件吗-interest@cs.oswego.edu想引起Doug的注意吗?我没有时间深入研究源代码,但我认为你是对的。我们在生产中不断地做到这一点。当我们将
CHM
初始容量增加到一个非常高的数字时,这种情况就消失了。在我们能够重构代码之前,我们将这样做。。。
for (Node<K,V>[] tab = table;;) {
    // ...
}