web-dev-qa-db-ja.com

静的初期化子で並列ストリームを使用すると、デッドロックが安定しない理由

注意:重複していません。トピックをよくお読みください- https://stackoverflow.com/users/3448419/apangin 見積もり:

本当の問題は、コードが機能しないのに機能することがある理由です。この問題は、ラムダがなくても再現されます。これにより、JVMのバグがあるのではないかと思います。

https://stackoverflow.com/a/53709217/26743 のコメントで、コードが開始ごとに異なる動作をする理由を見つけようとしましたが、そのディスカッションの参加者からアドバイスがありました。別のトピックを作成します。

次のソースコードを考えてみましょう。

public class Test {
    static {
        System.out.println("static initializer: " + Thread.currentThread().getName());

        final long SUM = IntStream.range(0, 5)
                .parallel()
                .mapToObj(i -> {
                    System.out.println("map: " + Thread.currentThread().getName() + " " + i);
                    return i;
                })
                .sum();
    }

    public static void main(String[] args) {
        System.out.println("Finished");
    }
}

時々(ほとんど常に)デッドロックにつながります。

出力の例:

static initializer: main
map: main 2
map: ForkJoinPool.commonPool-worker-3 4
map: ForkJoinPool.commonPool-worker-3 3
map: ForkJoinPool.commonPool-worker-2 0

しかし、時々それはうまく終了します(非常にまれです):

static initializer: main
map: main 2
map: main 3
map: ForkJoinPool.commonPool-worker-2 4
map: ForkJoinPool.commonPool-worker-1 1
map: ForkJoinPool.commonPool-worker-3 0
Finished

または

static initializer: main
map: main 2
map: ForkJoinPool.commonPool-worker-2 0
map: ForkJoinPool.commonPool-worker-1 1
map: ForkJoinPool.commonPool-worker-3 4
map: main 3

その振る舞いを説明してもらえますか?

19
gstackoverflow

TL; DRこれはHotSpotのバグです JDK-8215634

この問題は、レースがまったくない単純なテストケースで再現できます。

public class StaticInit {

    static void staticTarget() {
        System.out.println("Called from " + Thread.currentThread().getName());
    }

    static {
        Runnable r = new Runnable() {
            public void run() {
                staticTarget();
            }
        };

        r.run();

        Thread thread2 = new Thread(r, "Thread-2");
        thread2.start();
        try { thread2.join(); } catch (Exception ignore) {}

        System.out.println("Initialization complete");
    }

    public static void main(String[] args) {
    }
}

これは従来の初期化デッドロックのように見えますが、HotSpotJVMはハングしません。代わりに、次のように出力します。

Called from main
Called from Thread-2
Initialization complete

これがバグである理由

JVMS§6.5invokestaticバイトコードの実行時に

解決されたメソッドを宣言したクラスまたはインターフェイスは、そのクラスまたはインターフェイスがまだ初期化されていない場合に初期化されます

Thread-2staticTargetを呼び出すと、メインクラスStaticInitは明らかに初期化されていません(静的初期化子がまだ実行されているため)。これは、Thread-2JVMS§5.5 で説明されているクラス初期化手順を起動する必要があることを意味します。この手順によると、

  1. CのClassオブジェクトが、他のスレッドによるCの初期化が進行中であることを示している場合は、LC)を解放し、進行中の初期化が完了したことが通知されるまで現在のスレッドをブロックします。

ただし、クラスがスレッドmainによる初期化の進行中であるにもかかわらず、Thread-2はブロックされません。

他のJVMはどうですか

私はOpenJ9とJETをテストしましたが、どちらも上記のテストでデッドロックが発生することが予想されます。
HotSpotも-Xcompモードでハングしますが、-Xintまたは混合モードではハングしないのは興味深いことです。

それがどのように起こるか

インタプリタが最初にinvokestaticバイトコードに遭遇すると、JVMランタイムを呼び出してメソッド参照を解決します。このプロセスの一部として、JVMは必要に応じてクラスを初期化します。正常に解決された後、解決されたメソッドは定数プールキャッシュエントリに保存されます。定数プールキャッシュは、解決された定数プール値を格納するHotSpot固有の構造です。

上記のテストでは、invokestaticを呼び出すstaticTargetバイトコードは、最初にmainスレッドによって解決されます。クラスはすでに同じスレッドによって初期化されているため、インタープリターランタイムはクラスの初期化をスキップします。解決されたメソッドは、定数プールキャッシュに保存されます。次回Thread-2が同じinvokestaticを実行すると、インタプリタはバイトコードがすでに解決されていることを確認し、ランタイムを呼び出さずに定数プールキャッシュエントリを使用するため、クラスの初期化をスキップします。

getstatic/putstaticの同様のバグは、ずっと前に修正されました- JDK-449356 ですが、修正はinvokestaticに影響しませんでした。この問題に対処するために、新しいバグを送信しました JDK-8215634

元の例については、

ハングするかどうかは、どのスレッドが最初に静的呼び出しを解決するかによって異なります。 mainスレッドの場合、プログラムはデッドロックなしで完了します。静的呼び出しがForkJoinPoolスレッドのいずれかによって解決されると、プログラムはハングします。

更新

バグは 確認済み です。これは、次のリリースで修正されています:JDK 8u201、JDK 11.0.2、およびJDK12。

12
apangin