新しい値が読み取られた後に古い値を読み取る[重複]
この例を考えてみましょう。私たちは持っています:
int var = 0;
スレッドA:
System.out.println(var);
System.out.println(var);
スレッドB:
var = 1;
スレッドは同時に実行されます。次の出力は可能ですか?
1
0
つまり、新しい値が読み取られた後に元の値が読み取られます。var
揮発性ではありません。私の直感はそれが不可能だということです。
回答
あなたはSystem.out.println
それを内部的に使用しているsynchronized(this) {...}
ので、事態は少し悪化します。しかし、それでも、リーダースレッドは監視でき1, 0
ます。つまり、際どい読み取りです。
私はこれの専門家ではありませんが、Alexey Shipilevのビデオ/例/ブログをたくさん読んだ後、少なくとも何かを理解していると思います。
JLSは次のように述べています。
xとyが同じスレッドのアクションであり、プログラムの順序でxがyの前にある場合、hb(x、y)。
の両方の読み取りがでvar
あるため、次のようにprogram order
描画できます。
(po)
firstRead(var) ------> secondRead(var)
// po == program order
その文はまた、これがhappens-before
秩序を構築することを示しているので、次のようになります。
(hb)
firstRead(var) ------> secondRead(var)
// hb == happens before
しかし、それは「同じスレッド」内にあります。複数のスレッドについて推論したい場合は、同期の順序を調べる必要があります。についての同じ段落がhappens-before order
言うので、それが必要です:
アクションxが次のアクションyと同期する場合、hb(x、y)もあります。
したがって、との間でこの一連のアクションを構築するprogram order
とsynchronizes-with order
、結果について推論できます。それをあなたのコードに適用しましょう:
(NO SW) (hb)
write(var) ---------> firstRead(var) -------> secondRead(var)
// NO SW == there is "no synchronizes-with order" here
// hb == happens-before
そして、これは同じ章happens-before consistency
で活躍するところです:
一連のアクションAが発生します-一貫性を保つ前に、Aのすべての読み取りrについて、W(r)がrから見た書き込みアクションである場合、hb(r、W(r))またはwv = rvおよびhb(W(r)、w)およびhb(w、r)となるような書き込みwがAに存在します。
発生前の一貫した一連のアクションでは、各読み取りには、発生前に表示が許可されている書き込みが表示されます。
私は最初の文を非常に漠然と理解していることを認めます、そしてこれは彼がそれを言うように、アレクセイが私を最も助けてくれたところです:
読み取りは、
happens-before
または他の書き込みで発生した最後の書き込みを参照します。
そこには存在せずsynchronizes-with order
、暗黙的に存在しないhappens-before order
ため、読み取りスレッドはレースを介して読み取ることができます。これにより取得し1
、より0
。
正しいものを紹介するとすぐsynchronizes-with order
に、たとえばここから1つ
モニターmのロック解除アクションは同期します-後続のすべてのロックアクションは...
揮発性変数vへの書き込みは、任意のスレッドによるvの後続のすべての読み取りと同期します。
グラフが変化します(作成することを選択したとしましょうvar
volatile
):
SW PO
write(var) ---------> firstRead(var) -------> secondRead(var)
// SW == there IS "synchronizes-with order" here
// PO == happens-before
PO
(プログラムの順序)はHB
、JLSからのこの回答で引用した最初の文を介して(前に起こります)それを与えます。そしてSW
与えるHB
理由:
アクションxが次のアクションyと同期する場合、hb(x、y)もあります。
など:
HB HB
write(var) ---------> firstRead(var) -------> secondRead(var)
そして今、happens-before order
読み取りスレッドが「最後のHBで書かれた」された値を読みますか、それは読んでいることを意味することを言い1
、その後は0
不可能です。
私はサンプルのjcstressサンプルを取り、小さな変更を導入しました(あなたと同じようにSystem.out.println
):
@JCStressTest
@Outcome(id = "0, 0", expect = Expect.ACCEPTABLE, desc = "Doing both reads early.")
@Outcome(id = "1, 1", expect = Expect.ACCEPTABLE, desc = "Doing both reads late.")
@Outcome(id = "0, 1", expect = Expect.ACCEPTABLE, desc = "Doing first read early, not surprising.")
@Outcome(id = "1, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "First read seen racy value early, and the second one did not.")
@State
public class SO64983578 {
private final Holder h1 = new Holder();
private final Holder h2 = h1;
private static class Holder {
int a;
int trap;
}
@Actor
public void actor1() {
h1.a = 1;
}
@Actor
public void actor2(II_Result r) {
Holder h1 = this.h1;
Holder h2 = this.h2;
h1.trap = 0;
h2.trap = 0;
synchronized (this) {
r.r1 = h1.a;
}
synchronized (this) {
r.r2 = h2.a;
}
}
}
synchronized(this){....}
これは最初の例の一部ではないことに注意してください。同期して1, 0
も、結果としてそれを見ることができます。これは、synchronized
(から内部的にSystem.out.println
取得された)を使用しても、1
よりも取得できることを証明するためのもの0
です。
の値var
が読み取られた場合、1
元に戻ることはありません。この出力は、可視性や並べ替えのために発生することはありません。何が起こることができますすることで0 0
、0 1
と1 1
。
ここで理解する重要なポイントは、println
同期が関係しているということです。そのメソッドの内部を見ると、synchronized
そこに表示されるはずです。これらのブロックには、印刷がその順序で行われるという効果があります。書き込みはいつでも発生する可能性がありますが、最初の印刷での新しい値var
が表示され、2番目の印刷で古い値が表示されることはありません。したがって、書き込みは、両方の印刷の前、中間、または後にのみ発生する可能性があります。
それに加えて、var
マークが付けられてvolatile
おらず、書き込みが同期されていないため、書き込みが表示される保証はありません。
ここで欠けているのは、これらのスレッドが実際の物理コアで実行されているという事実であり、ここではいくつかの可能なバリアントがあります。
すべてのスレッドが同じコアで実行されると、問題はこれら3つの命令の実行順序になります。この場合、1,0は不可能だと思います。同期によって作成されたメモリバリアのために、printlnの実行は順序付けられます。 1,0を除く
AとBは2つの異なるコアで実行され、1,0も不可能に見えます。スレッドAを実行するコアが1を読み取るとすぐに、上記のprintlnが注文されたのと同じように、その後0を読み取る方法はありません。
スレッドAはこれら2つのprintlnの間で再スケジュールされるため、2番目のprintlnは、Bが実行されたのと同じか、別の3番目のコアで実行されます。したがって、2つのprintlnが異なるコアで実行される場合、2つのコアがどの値を参照するかによって異なります。varが同期されていない場合(varがこのメンバーであるかどうかが明確でない場合)、これら2つのコアは異なるvar値を参照できます。 1,0の可能性があります。
したがって、これはキャッシュコヒーレンスの問題です。
PS私はjvmの専門家ではないので、ここで他のことが行われている可能性があります。
他の答えに追加する:
long
およびを使用するとdouble
、書き込みはアトミックではない可能性があるため、最初の32ビットが最後の32ビットの前に表示される可能性があります。したがって、まったく異なる値が出力される可能性があります。