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


205

今日の私の研究室での敏感な操作は完全に間違っていました。電子顕微鏡のアクチュエーターはその境界を越え、一連の出来事の後に私は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のオーバーフロー時に符号付き整数が適切に動作することは誰もが知っているので、このコードに問題はないと思います。ただし、プログラムが終了条件に到達しなかったことを示す出力にもかかわらず、プログラムは終了条件に到達しました(到達しましたが、到達ませんでしたか?)。どうして?


これは一部の環境では発生しないことに気づきました。私は上だOpenJDKの 64ビットLinux上の6。


41
1200万の機器?私はそれがどのようにして起こるのか本当に興味があります...なぜあなたは空の同期ブロックを使用しているのですか?:synchronized(this){}?
マーティンV.

84
これは、リモートでスレッドセーフでさえありません。
マットボール

8
興味深いことに、final修飾子(生成されたバイトコードには影響しません)をフィールドに追加し、バグxy「解決」します。バイトコードには影響しませんが、フィールドにはフラグが立てられているため、JVM最適化の副作用であると思います。
Niv Steingarten 2013

9
@ユージーン:それは終わらないはずです。問題は「なぜそれが終わるのか」です。Aは、Point pこれを満たす構成されp.x+1 == p.y、その後、参照がポーリングスレッドに渡されます。最終的に、ポーリングスレッドPointは、受信したの1つで条件が満たされていないと判断して終了することを決定しますが、コンソール出力は、条件が満たされているはずであると示します。ここにないことは、volatile単にポーリングスレッドがスタックする可能性があることを意味しますが、それは明らかにここでは問題ではありません。
Erma K. Pizarro

21
@JohnNicholas:実際のコード(これは明らかにこれではありません)には100%のテストカバレッジと数千のテストがあり、その多くは何千ものさまざまな順序と順列でテストされました...テストは非決定論によって引き起こされたすべてのエッジケースを魔法のように見つけるわけではありませんJIT /キャッシュ/スケジューラー。本当の問題は、このコードを書いた開発者が、オブジェクトを使用する前に構築が行われないことを知らなかったことです。空synchronizedを削除するとバグが発生しないことに注意してください。これは、この動作を確定的に再現するコードが見つかるまで、ランダムにコードを記述しなければならなかったためです。

回答:


140

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

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

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

currentPosとしてマークするvolatileと、オブジェクトは実質的に不変であるため、安全な公開が保証されます。実際のユースケースでは、オブジェクトが構築後に変更された場合、volatile保証が十分ではなく、一貫性のないオブジェクトが再び表示される可能性があります。

または、をPoint使用しなくても、安全に公開できるように不変にすることもできますvolatile。不変性を実現するには、マークxしてyファイナルするだけです。

補足として、すでに述べたようにsynchronized(this) {}、JVMは何もしないものとして扱うことができます(動作を再現するために含めたと理解しています)。


4
よくわかりませんが、xとyをfinalにすると、メモリバリアを回避して同じ効果が得られませんか?
MichaelBöckling2013

3
よりシンプルな設計は、不変条件をテストする不変のポイントオブジェクトです。したがって、危険な構成を公開するリスクを負うことはありません。
Ron

@BuddyCasinoはい確かに-私はそれを追加しました。正直なところ、3か月前のディスカッション全体を覚えていません(finalの使用がコメントで提案されたので、オプションとして含めなかった理由がわかりません)。
アッシリア2013

2
不変性自体が安全な公開を保証するものではありません(xとyが非公開でゲッターのみで公開されている場合でも、同じ公開の問題が存在します)。finalまたはvolatileはそれを保証します。私は揮発性よりもファイナルを好むでしょう。
Steve Kuo

@SteveKuo不変性にはfinalが必要です。finalなしでは、同じセマンティクスを持たない効果的な不変性が最高です。
アッシリア2013

29

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

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

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

比較とその後の表示に使用するためにスレッド内で値を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);
}

2
はい、私はすべてをロックすることもできました。あなたのポイントは何ですか?

の使用に関するいくつかの追加説明を追加しましたvolatile
Ed Plese 2013

19

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

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

  • ピクセルに
  • pyする
  • currentposへ

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

Tがやっている:

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

mainの書き込みと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を揮発性にすることを提案しています。

  • x、yフィールドをfinalにすると、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が揮発性を保証するのに十分であると確信していなかったため、最初の回答は弱いメモリモデルを想定しました。Javaモデルがより強力であることを指摘して、assyliasからのコメントを反映するように編集された回答が発生します。 )。


2
これは私の意見では最良の説明です。どうもありがとう!
スカイド2013

1
@skydeですが、volatileのセマンティクスは間違っています。volatileは、volatile変数の読み取りが、volatile変数の最新の利用可能な書き込みとその前の書き込みを参照できることを保証します。この場合、をcurrentPos揮発性にすると、割り当てはcurrentPos、それら自体が揮発性でなくても、オブジェクトとそのメンバーの安全な公開を保証します。
アサイリア2013

ええと、私は私自身、JLSがvolatileが他の通常の読み取りと書き込みとの障壁を形成することを保証する方法を正確に見ることができないと言っていました。技術的には、私はそれについて間違っていることはできません;)。メモリモデルに関しては、順序が保証されておらず、他の方法よりも誤っており(まだ安全である)、間違っていて危険であると想定するのが賢明です。volatileがその保証を提供するのは素晴らしいことです。JLSの17章がそれをどのように提供するか説明できますか?
paulj 2013

2
つまり、ではPoint currentPos = new Point(x, y)、(w1)this.x = x、(w2)this.y = y、および(w3)の3つの書き込みがありますcurrentPos = the new point。プログラムの順序により、hb(w1、w3)およびhb(w2、w3)が保証されます。プログラムの後半で(r1)を読みますcurrentPoscurrentPosが揮発性でない場合、r1とw1、w2、w3の間にhbがないため、r1はそれらのいずれか(またはなし)を監視できます。volatileでは、hb(w3、r1)を導入します。また、hb関係は推移的であるため、hb(w1、r1)およびhb(w2、r1)も導入します。これは、Javaの同時実行性(3.5.3。安全な公開イディオム)にまとめられています。
アッシリアス2013

2
ああ、もしhbがそのように推移的であるなら、それは十分に強い「障壁」です、そうです。私が言わなければならないことは、JLSの17.4.5がhbにそのプロパティを持つと定義していることを決定するのは容易ではないということです。確かに、17.4.5の初め近くにあるプロパティのリストにはありません。推移的閉包は、いくつかの説明的な注記の後にさらに言及されています!とにかく、知っておくと良い、答えてくれてありがとう!:)。注:回答を更新して、assyliasのコメントを反映させます。
paulj 2013

-2

オブジェクトを使用して、書き込みと読み取りを同期させることができます。それ以外の場合、他の人が以前に言ったように、currentPosへの書き込みは2つの読み取りp.x + 1とpyの途中で発生します

new Thread() {
    void f(Point p) {
        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();
Object sem = new Object();
while (true) {
    synchronized(sem) {
        currentPos = new Point(currentPos.x+1, currentPos.y+1);
    }
}

実際、これは仕事をします。最初の試みでは、同期ブロック内に読み取りを配置しましたが、後でそれが本当に必要ではないことに気付きました。
Germano Fronza 2013

1
-1 JVMは、それsemが共有されていないことを証明し、同期されたステートメントをノーオペレーションとして扱うことができます...問題を解決するという事実は、幸運です。
アッシリア2013

4
私はマルチスレッドプログラミングが嫌いです。運が良ければ、多くのことがうまくいきます。
ジョナサンアレン

-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つだけ使用されることが保証されます。


currentPosは一度だけ読み込まれ、その値はpにコピーされます。pは2回読み取られますが、常に同じ場所を指しています。
ジョナサンアレン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.