web-dev-qa-db-ja.com

再帰的なConcurrentHashMap.computeIfAbsent()呼び出しは終了しません。バグまたは「機能」?

少し前に Java 8フィボナッチ数を再帰的に計算する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:\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、そのブログ投稿の読者も問題を確認しました(実際に発見しました)

これは変です。次の2つのいずれかを期待していました。

  • できます
  • ConcurrentModificationExceptionをスローします

しかし、停止することはありませんか?それは危険なようです。バグですか?または、いくつかの契約を誤解しましたか?

71
Lukas Eder

これは JDK-8062841 で修正されています。

2011の提案 で、コードのレビュー中にこの問題を特定しました。 JavaDocが更新され、一時的な修正が追加されました。パフォーマンスの問題により、さらに書き換えて削除されました。

2014ディスカッション で、検出と失敗を改善する方法を検討しました。議論の一部は、低レベルの変更を検討するためにオフラインでプライベートメールにされたことに注意してください。すべてのケースをカバーできるわけではありませんが、一般的なケースはライブロックされません。これらの fixes はDougのリポジトリにありますが、JDKリリースには含まれていません。

50
Ben Manes

これはもちろん "feature"です。 ConcurrentHashMap.computeIfAbsent() Javadocの読み取り:

指定されたキーがまだ値に関連付けられていない場合、指定されたマッピング関数を使用して値を計算し、nullでない限りこのマップに入力します。メソッド呼び出し全体がアトミックに実行されるため、関数はキーごとに最大1回適用されます。このマップで他のスレッドが試行した更新操作の一部は、計算の進行中にブロックされる可能性があるため、計算は短く単純である必要があり、は更新を試みてはなりませんこのマップの他のマッピング

"must not"という文言は明確な契約であり、私の同時性の理由ではありませんが、私のアルゴリズムに違反しました。

まだ興味深いのは、ConcurrentModificationExceptionがないことです。代わりに、プログラムが停止することはありません-これは私の意見ではまだかなり危険なバグです(つまり 無限ループ。または:うまくいかない可能性のあるものは何でも )。

注意:

HashMap.computeIfAbsent() または Map.computeIfAbsent() Javadocは、このような再帰的な計算を禁止しません。これはもちろん、キャッシュのタイプとしてばかげていますMap<Integer, Integer>ではなくConcurrentHashMap<Integer, Integer>です。サブタイプがスーパータイプコントラクトを大幅に再定義することは非常に危険です(Set vs. SortedSetはあいさつです)。 したがって、スーパータイプでも、そのような再帰を実行することは禁止されるべきです。

53
Lukas Eder

これはバグに非常に似ています。なぜなら、キャパシティが32のキャッシュを作成すると、プログラムは49まで動作するからです。興味深いことに、そのパラメータsizeCtl = 32 +(32 >>> 1)+ 1)= 49!サイズ変更の理由がありますか?