208

今日、私の研究室で機密扱いの操作が完全に失敗しました。電子顕微鏡のアクチュエーターが限界を超え、一連の出来事の後で 1,200 万ドルの機器を失いました。障害のあるモジュールの 40,000 行以上を次のように絞り込みました。

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 を使用しています。

4

5 に答える 5

29

currentPosはスレッド外で変更されているため、次のようにマークする必要がありますvolatile

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

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

スレッド内で 1 回だけ値を読み取って比較とその後の表示に使用すると、結果は大きく異なって見えます。私が行うと、x常に次のように表示され、いくつかの大きな整数の間で1変化yします。0この時点での動作は、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);
}
于 2013-04-23T02:06:45.193 に答える
19

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

メインスレッドは次の書き込みを行っています (ポイントの初期設定を無視すると、px と py がデフォルト値になります):

  • ピクセルに
  • パイする
  • 現在の位置へ

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

したがって、T は次のことを行っています。

  1. currentpos を p に読み込みます
  2. px と py を (どちらかの順序で) 読み取ります
  3. 比較し、枝を取る
  4. px と py (どちらの順序でも) を読み取り、System.out.println を呼び出します

メインの書き込みと T の読み取りの間に順序関係がないことを考えると、T はcurrentpos.y または currentpos.x への書き込みの前にmain の currentpos への書き込みを確認する可能性があるため、明らかにいくつかの方法で結果が生成されます。

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

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

ここでの誤った仮定は、この行から生じる書き込みが、それを実行するスレッドのプログラム順序ですべてのスレッドにわたって可視化されると考えていることだと思います。

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

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

  • x、y フィールドを final にすると、Java は、すべてのスレッドで、コンストラクターが戻る前にそれらの値の書き込みが発生することを保証します。したがって、currentpos への代入はコンストラクターの後であるため、T スレッドは正しい順序で書き込みを確認することが保証されます。
  • currentpos を volatile にすると、Java は、これが同期ポイントであり、他の同期ポイントに対して完全に順序付けられることを保証します。main と同様に、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 モデルがより強力であることを指摘しています。 )。

于 2013-08-15T17:54:00.287 に答える
-3

currentPos に 2 回アクセスしており、これら 2 回のアクセスの間に更新されないという保証はありません。

例えば:

  1. x = 10、y = 11
  2. ワーカー スレッドは px を 10 と評価します
  3. メイン スレッドが更新を実行します。現在は x = 11 および y = 12 です。
  4. ワーカー スレッドは py を 12 と評価します
  5. ワーカー スレッドは 10+1 != 12 であることを認識するため、出力して終了します。

基本的に、 2 つの異なるポイントを比較しています。

currentPos を volatile にしても、ワーカー スレッドによる 2 つの個別の読み取りであるため、この問題から保護されないことに注意してください。

を追加

boolean IsValid() { return x+1 == y; }

あなたのポイントクラスへのメソッド。これにより、x+1 == y をチェックするときに currentPos の値が 1 つだけ使用されるようになります。

于 2013-08-15T18:10:46.567 に答える