web-dev-qa-db-ja.com

どうしてこのJavaプログラムは、どうやらそうすべきではない(そしてそうしなかった)のに、なぜ終了するのですか?

今日、私のラボでのデリケートな操作は完全に間違っていました。電子顕微鏡のアクチュエータがその境界を越え、一連の出来事の後、1200万ドルの機器を失いました。障害のあるモジュールの40K行をこれに絞り込みました。

import Java.util.*;

class A {
    static Point currentPos = new Point(1,2);
    static class Point {
        int x;
        int y;
        Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }
    public static void main(String[] args) {
        new Thread() {
            void f(Point p) {
                synchronized(this) {}
                if (p.x+1 != p.y) {
                    System.out.println(p.x+" "+p.y);
                    System.exit(1);
                }
            }
            @Override
            public void run() {
                while (currentPos == null);
                while (true)
                    f(currentPos);
            }
        }.start();
        while (true)
            currentPos = new Point(currentPos.x+1, currentPos.y+1);
    }
}

私が得ている出力のサンプル:

$ Java A
145281 145282
$ Java A
141373 141374
$ Java A
49251 49252
$ Java A
47007 47008
$ Java A
47427 47428
$ Java A
154800 154801
$ Java A
34822 34823
$ Java A
127271 127272
$ Java A
63650 63651

ここには浮動小数点演算がなく、Javaでのオーバーフロー時に符号付き整数が適切に動作することがわかっているため、このコードには何も問題はないと思います。ただし、プログラムが終了条件に到達しなかったことを示す出力にもかかわらず、終了条件に到達しました(到達しました未到達?) 。どうして?


環境によってはこれが起こらないことに気付きました。 64ビットLinuxでは OpenJDK 6です。

205
Dog

明らかにcurrentPosへの書き込みは、その読み取りの前には発生しませんが、それがどのように問題になるかわかりません。

currentPos = new Point(currentPos.x+1, currentPos.y+1);は、デフォルト値をxおよびy(0)に書き込んでから、コンストラクターに初期値を書き込むなど、いくつかのことを行います。オブジェクトは安全に公開されていないため、これらの4つの書き込み操作は、コンパイラ/ JVMによって自由に並べ替えることができます。

したがって、読み取りスレッドの観点から見ると、xを新しい値で読み取り、yをデフォルト値の0で読み取るのは正当な実行です。 printlnステートメントに到達するまでに(これは同期されているため、読み取り操作に影響します)、変数には初期値があり、プログラムは期待値を出力します。

currentPosvolatileとしてマークすると、オブジェクトが事実上不変であるため、安全な公開が保証されます-実際の使用例では、構築後にオブジェクトが変更された場合、volatile保証は十分ではなく、一貫性のないオブジェクトをもう一度参照してください。

または、Pointを不変にすることもできます。これにより、volatileを使用しなくても、安全な公開が保証されます。不変性を実現するには、単にxおよびy finalをマークする必要があります。

補足説明として、また既に述べたように、synchronized(this) {}はJVMによってノーオペレーションとして扱われます(動作を再現するためにそれを含めたと理解しています)。

140
assylias

currentPosはスレッド外で変更されるため、volatileとしてマークする必要があります。

static volatile Point currentPos = new Point(1,2);

Volatileがない場合、スレッドはメインスレッドで行われているcurrentPosの更新を読み込むことが保証されません。したがって、currentPosの新しい値は引き続き書き込まれますが、パフォーマンス上の理由から、スレッドは以前のキャッシュバージョンを引き続き使用します。 currentPosを変更するスレッドは1つだけなので、ロックなしで逃げることができ、パフォーマンスが向上します。

比較とその後の表示に使用するスレッド内で一度だけ値を読み取った場合、結果は大きく異なります。次のことを行うと、xは常に1として表示され、y0といくつかの大きな整数の間で変化します。この時点でのvolatileキーワードがない場合の動作は未定義であり、コードのJITコンパイルがこのような動作に寄与している可能性があると思います。また、空のsynchronized(this) {}ブロックをコメントアウトすると、コードも機能します。ロックが原因でcurrentPosとそのフィールドがキャッシュから使用されるのではなく再読み取りされるのに十分な遅延が生じるためと思われます。

int x = p.x + 1;
int y = p.y;

if (x != y) {
    System.out.println(x+" "+y);
    System.exit(1);
}
29
Ed Plese

通常のメモリ、「currentpos」参照、およびその背後にあるPointオブジェクトとそのフィールドがあり、2つのスレッド間で同期なしで共有されています。したがって、メインスレッドのこのメモリに発生する書き込みと、作成されたスレッドの読み取り(Tと呼ぶ)の間に定義された順序はありません。

メインスレッドは次の書き込みを行っています(ポイントの初期設定を無視すると、p.xおよびp.yがデフォルト値になります)。

  • p.xへ
  • currentposへ

同期/バリアに関してこれらの書き込みについて特別なものはないため、ランタイムはTスレッドがそれらを任意の順序で発生させることを許可します(もちろん、メインスレッドは常にプログラムの順序に従って書き込みと読み取りを確認します)。 Tの読み取り間の任意の時点で.

だからTはやっている:

  1. currentposをpに読み取ります
  2. p.xとp.yを読む(どちらの順序でも)
  3. 比較し、分岐を取る
  4. p.xおよびp.y(いずれかの順序)を読み取り、System.out.printlnを呼び出します

Mainの書き込みとTの読み取りの間に順序関係がないため、Tがmainのcurrentposへの書き込みbeforecurrentpos.yまたはcurrentpos.xへの書き込み:

  1. Xの書き込みが発生する前にcurrentpos.xを読み取ります-0を取得し、yの書き込みが発生する前にcurrentpos.yを読み取ります-0を取得します。evalsをtrueと比較します。書き込みはTから見えるようになります。System.out.printlnが呼び出されます。
  2. Xの書き込みが発生した後、最初にcurrentpos.xを読み取り、yの書き込みが発生する前にcurrentpos.yを読み取ります-0を取得します。evalsをtrueと比較します。書き込みはT ...に見えるようになります。
  3. Yの書き込みが発生する前(0)に最初にcurrentpos.yを読み取り、xの書き込み後にcurrentpos.xを読み取り、trueに評価します。等.

など...ここには多くのデータ競合があります。

ここでの欠陥のある仮定は、この行から生じる書き込みが、それを実行するスレッドのプログラム順序ですべてのスレッドにわたって見えるようになっていると考えていることを疑っています:

currentPos = new Point(currentPos.x+1, currentPos.y+1);

Javaはそのような保証を行いません(パフォーマンスにはひどいでしょう)。プログラムが他のスレッドでの読み取りに対する書き込みの保証された順序を必要とする場合は、さらに何かを追加する必要があります。他の人は、x、yフィールドを最終的にするか、あるいはcurrentposをvolatileにすることを提案しています。

  • X、yフィールドを最終にする場合、Javaは、すべてのスレッドでコンストラクターが戻る前に値の書き込みが発生することを保証します。したがって、currentposへの割り当てはコンストラクターの後にあるため、Tスレッドは正しい順序で書き込みを確認することが保証されます。
  • Currentposを揮発性にする場合、Javaは、これが他の同期ポイントに対して完全に順序付けられる同期ポイントであることを保証します。主に、xとyへの書き込みはcurrentposへの書き込みの前に発生する必要があるため、別のスレッドでのcurrentposの読み取りは、前に発生したx、yの書き込みも確認する必要があります。

Finalを使用すると、フィールドが不変になるため、値をキャッシュできるという利点があります。 volatileを使用すると、currentposのすべての書き込みおよび読み取りで同期が行われ、パフォーマンスが低下する可能性があります。

厄介な詳細については、Java言語仕様の第17章を参照してください。 http://docs.Oracle.com/javase/specs/jls/se7/html/jls-17.html =

(JLSがvolatileを保証していると確信できなかったため、最初の回答はより弱いメモリモデルを想定していました。assyliasからのコメントを反映するために編集された回答は、Java currentposでも揮発性で十分です)。

19
paulj