新しい値が読み取られた後に古い値を読み取る[重複]

Nov 24 2020

この例を考えてみましょう。私たちは持っています:

int var = 0;

スレッドA:

System.out.println(var);
System.out.println(var);

スレッドB:

var = 1;

スレッドは同時に実行されます。次の出力は可能ですか?

1
0

つまり、新しい値が読み取られた後に元の値が読み取られます。var揮発性ではありません。私の直感はそれが不可能だということです。

回答

3 Eugene Nov 26 2020 at 11:41

あなたは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 ordersynchronizes-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です。

1 akuzminykh Nov 24 2020 at 17:04

の値varが読み取られた場合、1元に戻ることはありません。この出力は、可視性や並べ替えのために発生することはありません。何が起こることができますすることで0 00 11 1

ここで理解する重要なポイントは、println同期が関係しているということです。そのメソッドの内部を見ると、synchronizedそこに表示されるはずです。これらのブロックには、印刷がその順序で行われるという効果があります。書き込みはいつでも発生する可能性がありますが、最初の印刷での新しい値varが表示され、2番目の印刷で古い値が表示されることはありません。したがって、書き込みは、両方の印刷の前、中間、または後にのみ発生する可能性があります。

それに加えて、varマークが付けられてvolatileおらず、書き込みが同期されていないため、書き込みが表示される保証はありません。

AlexRevetchi Nov 26 2020 at 15:32

ここで欠けているのは、これらのスレッドが実際の物理コアで実行されているという事実であり、ここではいくつかの可能なバリアントがあります。

  1. すべてのスレッドが同じコアで実行されると、問題はこれら3つの命令の実行順序になります。この場合、1,0は不可能だと思います。同期によって作成されたメモリバリアのために、printlnの実行は順序付けられます。 1,0を除く

  2. AとBは2つの異なるコアで実行され、1,0も不可能に見えます。スレッドAを実行するコアが1を読み取るとすぐに、上記のprintlnが注文されたのと同じように、その後0を読み取る方法はありません。

  3. スレッドAはこれら2つのprintlnの間で再スケジュールされるため、2番目のprintlnは、Bが実行されたのと同じか、別の3番目のコアで実行されます。したがって、2つのprintlnが異なるコアで実行される場合、2つのコアがどの値を参照するかによって異なります。varが同期されていない場合(varがこのメンバーであるかどうかが明確でない場合)、これら2つのコアは異なるvar値を参照できます。 1,0の可能性があります。

したがって、これはキャッシュコヒーレンスの問題です。

PS私はjvmの専門家ではないので、ここで他のことが行われている可能性があります。

FrancescoMenzani Nov 25 2020 at 01:35

他の答えに追加する:

longおよびを使用するとdouble、書き込みはアトミックではない可能性があるため、最初の32ビットが最後の32ビットの前に表示される可能性があります。したがって、まったく異なる値が出力される可能性があります。