9

「Java Concurrency in Practice」では、Java メモリ モデルの性質により、永久に実行されるか、0 を出力する可能性がある安全でないクラスの例を次に示します。

このクラスが実証しようとしている問題は、ここの変数がスレッド間で「共有」されていないことです。そのため、スレッドが参照する値は、揮発性または同期化されていないため、別のスレッドとは異なる場合があります。また、JVM によって許可されるステートメントの並べ替えにより、ready=true が number=42 より前に設定される場合があります。

私にとって、このクラスは常に JVM 1.6 を使用して正常に動作します。このクラスが正しくない動作を実行する方法 (つまり、0 を出力するか、永久に実行する) に関するアイデアはありますか?

public class NoVisibility {
    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread {
        public void run() {
            while (!ready)
                Thread.yield();
            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
}
4

6 に答える 6

6

問題は、コードが最適化されて値がキャッシュされるまで十分に待機していないことです。

x86_64 システムのスレッドが初めて値を読み取るとき、スレッドセーフなコピーを取得します。その後の変更のみが表示されない可能性があります。これは、他の CPU では当てはまらない場合があります。

これを試すと、各スレッドがそのローカル値でスタックしていることがわかります。

public class RequiresVolatileMain {
    static volatile boolean value;

    public static void main(String... args) {
        new Thread(new MyRunnable(true), "Sets true").start();
        new Thread(new MyRunnable(false), "Sets false").start();
    }

    private static class MyRunnable implements Runnable {
        private final boolean target;

        private MyRunnable(boolean target) {
            this.target = target;
        }

        @Override
        public void run() {
            int count = 0;
            boolean logged = false;
            while (true) {
                if (value != target) {
                    value = target;
                    count = 0;
                    if (!logged)
                        System.out.println(Thread.currentThread().getName() + ": reset value=" + value);
                } else if (++count % 1000000000 == 0) {
                    System.out.println(Thread.currentThread().getName() + ": value=" + value + " target=" + target);
                    logged = true;
                }
            }
        }
    }
}

値の反転を示す次のように出力しますが、スタックします。

Sets true: reset value=true
Sets false: reset value=false
...
Sets true: reset value=true
Sets false: reset value=false
Sets true: value=false target=true
Sets false: value=true target=false
....
Sets true: value=false target=true
Sets false: value=true target=false

このスイッチを追加すると-XX:+PrintCompilation、あなたが見る頃に起こります

1705    1 % RequiresVolatileMain$MyRunnable::run @ -2 (129 bytes)   made not entrant
1705    2 % RequiresVolatileMain$MyRunnable::run @ 4 (129 bytes)

コードがネイティブにコンパイルされたことを示唆するものは、スレッドセーフではない方法です。

値を作成すると、volatile値が際限なく(または飽きるまで)反転することがわかります

編集:このテストが行​​うことは次のとおりです。値がスレッドのターゲット値ではないことを検出すると、値を設定します。すなわち。スレッド 0 が に設定されtrue、スレッド 1 が に設定されますfalse 。2 つのスレッドがフィールドを適切に共有している場合、互いの変更が確認され、値が常に true と false の間で反転します。

volatile がないと、これは失敗し、各スレッドは独自の値のみを参照するため、値を変更し、スレッド 0trueとスレッド 1の両方falseが同じフィールドを参照します。

于 2012-01-22T13:15:05.227 に答える
6

Java メモリ モデルは、機能するために必要なものとそうでないものを定義します。安全でないマルチスレッド コードの「美しさ」は、ほとんどの状況 (特に制御された開発環境) で通常は機能することです。より優れたコンピューターを使用して本番環境に移行し、負荷が増加し、JIT が実際に開始されて初めて、バグが発生し始めます。

于 2012-01-22T13:13:05.640 に答える
2

これについて 100% 確信があるわけではありませんが、次のことが関連している可能性があります。

並び替え とはどういう意味ですか?

プログラム変数 (オブジェクト インスタンス フィールド、クラス静的フィールド、および配列要素) へのアクセスが、プログラムで指定された順序とは異なる順序で実行されるように見える場合が多数あります。コンパイラは、最適化という名目で命令の順序を自由に決めることができます。プロセッサは、特定の状況下で命令を順不同で実行する場合があります。データは、プログラムで指定された順序とは異なる順序で、レジスタ、プロセッサ キャッシュ、およびメイン メモリ間で移動される場合があります。

たとえば、スレッドがフィールド a に書き込み、次にフィールド b に書き込み、b の値が a の値に依存しない場合、コンパイラはこれらの操作を自由に並べ替え、キャッシュは b をメインに自由にフラッシュできます。 A以前の記憶。コンパイラ、JIT、キャッシュなど、並べ替えの潜在的な原因は多数あります。

コンパイラ、ランタイム、およびハードウェアは共謀して as-if-serial セマンティクスの錯覚を作成することになっています。つまり、シングル スレッド プログラムでは、プログラムは並べ替えの影響を観察できないはずです。ただし、再順序付けは、正しく同期されていないマルチスレッド プログラムで発生する可能性があります。この場合、1 つのスレッドが他のスレッドの影響を観察でき、変数アクセスが実行または指定された順序とは異なる順序で他のスレッドから見えるようになることを検出できる場合があります。プログラム

于 2012-01-22T13:12:07.083 に答える
2

これについての主なポイントは、すべての jvm が同じ方法で命令を並べ替えることが保証されていないということだと思います。これは、さまざまな可能な並べ替えが存在する例として使用されているため、jvm の一部の実装では、異なる結果が得られる可能性があります。jvm が毎回同じように並べ替えられているのはたまたまですが、別の場合はそうではないかもしれません。順序を保証する唯一の方法は、適切な同期を使用することです。

于 2012-01-22T13:22:04.260 に答える
1

OSによっては、Thread.yield()が機能する場合と機能しない場合があります。Thread.yield()は、実際にはプラットフォームに依存しないと見なすことはできないため、その仮定が必要な場合は使用しないでください。

例に期待どおりの動作をさせると、それは他の何よりもプロセッサアーキテクチャの問題だと思います...さまざまなOSを使用して、さまざまなマシンで実行してみて、何が得られるかを確認してください。

于 2012-01-22T13:09:23.023 に答える