今日、私のラボでのデリケートな操作は完全に間違っていました。電子顕微鏡のアクチュエータがその境界を越え、一連の出来事の後、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です。
明らかに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
finalをマークする必要があります。
補足説明として、また既に述べたように、synchronized(this) {}
はJVMによってノーオペレーションとして扱われます(動作を再現するためにそれを含めたと理解しています)。
currentPos
はスレッド外で変更されるため、volatile
としてマークする必要があります。
static volatile Point currentPos = new Point(1,2);
Volatileがない場合、スレッドはメインスレッドで行われているcurrentPosの更新を読み込むことが保証されません。したがって、currentPosの新しい値は引き続き書き込まれますが、パフォーマンス上の理由から、スレッドは以前のキャッシュバージョンを引き続き使用します。 currentPosを変更するスレッドは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);
}
通常のメモリ、「currentpos」参照、およびその背後にあるPointオブジェクトとそのフィールドがあり、2つのスレッド間で同期なしで共有されています。したがって、メインスレッドのこのメモリに発生する書き込みと、作成されたスレッドの読み取り(Tと呼ぶ)の間に定義された順序はありません。
メインスレッドは次の書き込みを行っています(ポイントの初期設定を無視すると、p.xおよびp.yがデフォルト値になります)。
同期/バリアに関してこれらの書き込みについて特別なものはないため、ランタイムはTスレッドがそれらを任意の順序で発生させることを許可します(もちろん、メインスレッドは常にプログラムの順序に従って書き込みと読み取りを確認します)。 Tの読み取り間の任意の時点で.
だからTはやっている:
Mainの書き込みとTの読み取りの間に順序関係がないため、Tがmainのcurrentposへの書き込みbeforecurrentpos.yまたはcurrentpos.xへの書き込み:
など...ここには多くのデータ競合があります。
ここでの欠陥のある仮定は、この行から生じる書き込みが、それを実行するスレッドのプログラム順序ですべてのスレッドにわたって見えるようになっていると考えていることを疑っています:
currentPos = new Point(currentPos.x+1, currentPos.y+1);
Javaはそのような保証を行いません(パフォーマンスにはひどいでしょう)。プログラムが他のスレッドでの読み取りに対する書き込みの保証された順序を必要とする場合は、さらに何かを追加する必要があります。他の人は、x、yフィールドを最終的にするか、あるいはcurrentposをvolatileにすることを提案しています。
Finalを使用すると、フィールドが不変になるため、値をキャッシュできるという利点があります。 volatileを使用すると、currentposのすべての書き込みおよび読み取りで同期が行われ、パフォーマンスが低下する可能性があります。
厄介な詳細については、Java言語仕様の第17章を参照してください。 http://docs.Oracle.com/javase/specs/jls/se7/html/jls-17.html =
(JLSがvolatileを保証していると確信できなかったため、最初の回答はより弱いメモリモデルを想定していました。assyliasからのコメントを反映するために編集された回答は、Java currentposでも揮発性で十分です)。