!=チェックスレッドは安全ですか?


140

などの複合操作i++複数の操作を伴うため、スレッドセーフではありません。

しかし、それ自体で参照をチェックすることはスレッドセーフな操作ですか?

a != a //is this thread-safe

私はこれをプログラムして複数のスレッドを使用しようとしましたが、失敗しませんでした。私のマシンではレースをシミュレートできなかったと思います。

編集:

public class TestThreadSafety {
    private Object a = new Object();

    public static void main(String[] args) {

        final TestThreadSafety instance = new TestThreadSafety();

        Thread testingReferenceThread = new Thread(new Runnable() {

            @Override
            public void run() {
                long countOfIterations = 0L;
                while(true){
                    boolean flag = instance.a != instance.a;
                    if(flag)
                        System.out.println(countOfIterations + ":" + flag);

                    countOfIterations++;
                }
            }
        });

        Thread updatingReferenceThread = new Thread(new Runnable() {

            @Override
            public void run() {
                while(true){
                    instance.a = new Object();
                }
            }
        });

        testingReferenceThread.start();
        updatingReferenceThread.start();
    }

}

これは、スレッドの安全性をテストするために使用しているプログラムです。

奇妙な行動

プログラムがいくつかの反復の間に開始すると、出力フラグの値を取得します。これは!=、同じ参照で参照チェックが失敗することを意味します。しかし、いくつかの反復の後、出力は一定値にfalseなり、プログラムを長時間実行しても、単一のtrue出力は生成されません。

いくつかのn(固定されていない)反復の後、出力が示唆するように、出力は定数値であるように見え、変化しません。

出力:

いくつかの反復について:

1494:true
1495:true
1496:true
19970:true
19972:true
19974:true
//after this there is not a single instance when the condition becomes true

2
この文脈で「スレッドセーフ」とはどういう意味ですか?常にfalseを返すことが保証されているかどうかを尋ねていますか?
JBニゼット2013

@JBNizetはい。それは私が考えていたものです。
Narendra Pathai 2013

5
シングルスレッドのコンテキストでは、常にfalseを返すとは限りません。NaNの可能性があります
。–ハロルド

4
考えられる説明:コードはジャストインタイムでコンパイルされ、コンパイルされたコードは変数を1回だけロードします。これは予想通りです。
Marko Topolnik 2013

3
個々の結果を印刷することは、レースをテストするための悪い方法です。印刷(フォーマットと結果の書き込みの両方)は、テストに比べて比較的コストがかかります(そして、端末または端末自体への接続の帯域幅が遅い場合、プログラムが書き込みをブロックすることがあります)。また、IOには、スレッドの実行順序を入れ替える独自のmutexが含まれていることがよくあります(個別の行が互いに1234:trueスマッシュしないことに注意してください)。レーステストには、より厳しい内部ループが必要です。最後に要約を印刷します(誰かが単体テストフレームワークで以下に行ったように)。
ベンジャクソン

回答:


124

同期がない場合、このコード

Object a;

public boolean test() {
    return a != a;
}

生成する可能性がありtrueます。これはのバイトコードですtest()

    ALOAD 0
    GETFIELD test/Test1.a : Ljava/lang/Object;
    ALOAD 0
    GETFIELD test/Test1.a : Ljava/lang/Object;
    IF_ACMPEQ L1
...

フィールドaをローカル変数に2回ロードしていることがわかるのでa、別のスレッドによって変更された場合、非アトミック操作falseです。

また、ここではメモリの可視性の問題が関係しており、a別のスレッドによって行われた変更が現在のスレッドに表示されるという保証はありません。


22
強力な証拠ですが、バイトコードは実際には証拠ではありません。それはJLSのどこかにあるに違いない...
Marko Topolnik 2013

10
@Marko私はあなたの考えに同意しますが、必ずしもあなたの結論には同意しません。私にとって、上記のバイトコードは!=、LHSとRHSを別々にロードすることを含む、明白な/正規の実装方法です。そして、LHSとRHSが構文的に同一であるときに、JLS 最適化について具体的なことを何も述べていない場合、一般的な規則が適用されますa。つまり、2回ロードすることになります。
Andrzej Doyle

20
実際、生成されたバイトコードがJLSに準拠していると仮定すると、それは証明です!
proskor 2013

6
@エイドリアン:まず、その仮定が無効であっても、「true」と評価できる単一のコンパイラが存在すれば、「true」と評価できることを示すのに十分です(仕様で禁止されている場合でも、しない)。第二に、Javaは十分に仕様化されており、ほとんどのコンパイラーはJavaに厳密に準拠しています。この点でそれらを参照として使用することは理にかなっています。第3に、「JRE」という用語を使用しますが、それがあなたの考えていることを意味しているとは思いません。。。
ruakh 2013

2
@AlexanderTorstling- 「シングルリードの最適化を排除するのにそれで十分かどうかはわかりません。」 それは十分ではありません。実際には、同期が存在しない場合に、最適化が有効である(と余分課す関係「の前に起こる」)
スティーブン・C

47

チェックはa != aスレッドセーフですか?

a(適切な同期なしで)別のスレッドによって更新される可能性がある場合は、いいえ。

私はこれをプログラムして複数のスレッドを使用しようとしましたが、失敗しませんでした。私のマシンではレースをシミュレートできなかったと思います。

それは何の意味もありません!問題は、a別のスレッドによって更新される実行がJLSによって許可されている場合、コードはスレッドセーフではないということです。特定のマシンおよび特定のJava実装の特定のテストケースで競合状態を発生させることができないという事実は、それが他の状況で発生することを排除するものではありません。

これは!=を返すことができることを意味していますtrue

はい、理論的には、特定の状況下で。

または、同時に変更されていてもa != a戻ることができます。falsea


「奇妙な振る舞い」について:

プログラムがいくつかの反復の間に開始されると、出力フラグ値を取得します。これは、参照!=チェックが同じ参照で失敗することを意味します。しかし、いくつかの反復の後、出力は定数値falseになり、長時間プログラムを実行しても、単一のtrue出力は生成されません。

この「奇妙な」動作は、次の実行シナリオと一致しています。

  1. プログラムがロードされ、JVMがバイトコードの解釈を開始します。(javapの出力からわかるように)バイトコードは2つのロードを実行するため、(明らかに)競合状態の結果がときどき表示されます。

  2. しばらくすると、コードはJITコンパイラーによってコンパイルされます。JITオプティマイザは、同じメモリスロット(a)の2つのロードが互いに接近していることを認識し、2つ目のロードを最適化します。(実際、テストを完全に最適化する可能性があります...)

  3. これで、2つの負荷がなくなったため、競合状態が発生しなくなりました。

これはすべて、JLSがJavaの実装で実行できることと一致しています。


@krissはこうコメントしました:

これは、CまたはC ++プログラマが「未定義の動作」と呼ぶものである可能性があるように見えます(実装に依存)。このようなコーナーケースでは、JavaにいくつかのUBが存在するようです。

Javaメモリモデル(JLS 17.4で指定)は、1つのスレッドが別のスレッドによって書き込まれたメモリ値を参照できることが保証されている一連の前提条件を指定します。あるスレッドが別のスレッドが書き込んだ変数を読み取ろうとしたときに、これらの前提条件が満たされていない場合、実行が多数発生する可能性があります。その一部は正しくない可能性があります(アプリケーションの要件の観点から)。言い換えれば、セット可能な行動(「整形式実行」のセットをIE)のが定義されているが、我々は発生します、それらの行動のどのと言うことはできません。

コードの最終的な効果が同じであれば、コンパイラーはロードの結合と並べ替え、保存(およびその他の処理)を実行できます。

  • シングルスレッドで実行された場合
  • 正しく同期するさまざまなスレッドによって実行されたとき(メモリモデルごと)。

しかし、コードが適切に同期しない場合(したがって、「前に発生」の関係では、整形式の実行のセットが十分に制約されない場合)、コンパイラは、「不正な」結果をもたらすような方法でロードとストアを並べ替えることができます。(しかし、それは実際にプログラムが正しくないことを言っているだけです。)


これはa != atrueを返す可能性があることを意味しますか?
proskor 2013

たぶん私のマシンでは、上のコードがスレッドセーフでないことをシミュレートできなかったことを意味しました。おそらくその背後に理論的な推論があるのか​​もしれません。
Narendra Pathai 2013

@NarendraPathai-あなたがそれを実証できない理論的な理由はありません。おそらく実際的な理由があります...または、運が悪かっただけかもしれません。
スティーブンC

私が使用しているプログラムで私の最新の回答を確認してください。チェックはtrueを返すことがありますが、出力に奇妙な動作があるようです。
Narendra Pathai 2013

1
@NarendraPathai-私の説明を参照してください。
スティーブンC

27

test-ngで証明:

public class MyTest {

  private static Integer count=1;

  @Test(threadPoolSize = 1000, invocationCount=10000)
  public void test(){
    count = new Integer(new Random().nextInt());
    Assert.assertFalse(count != count);
  }

}

10 000回の呼び出しで2回失敗しています。だから、いいえ、それはスレッドセーフではありません


6
等しいかどうかさえチェックしていません...そのRandom.nextInt()部分は不必要です。new Object()同様にテストすることもできます。
Marko Topolnik 2013

@MarkoTopolnik私が使用しているプログラムで更新された回答を確認してください。チェックはtrueを返すことがありますが、出力に奇妙な動作があるようです。
Narendra Pathai 2013

1
余談ですが、ランダムオブジェクトは通常、再利用するためのものであり、新しいintが必要になるたびに作成されるわけではありません。
Simon Forsberg、

15

いいえそうではありません。比較の場合、Java VMは2つの値をスタックで比較し、比較命令を実行する必要があります(どちらが「a」のタイプに依存するか)。

Java VMは次のことを行います。

  1. 「a」を2回読み取り、それぞれをスタックに入れてから、結果を比較します
  2. 「a」を1回だけ読み取り、それをスタックに入れて複製し(「dup」命令)、比較を実行する
  3. 式を完全に削除して、次のものに置き換えます false

最初のケースでは、別のスレッドが2つの読み取りの間の "a"の値を変更する可能性があります。

どの戦略を選択するかは、JavaコンパイラとJavaランタイム(特にJITコンパイラ)によって異なります。プログラムの実行中に変更される場合もあります。

変数へのアクセス方法を確認する場合は、変数を作成するvolatile(いわゆる「ハーフメモリバリア」)か、完全なメモリバリアを追加する必要があります(synchronized)。より高いレベルのAPIを使用することもできます(たとえばAtomicInteger、June Ahasanが言及)。

スレッドセーフティの詳細については、JSR 133Javaメモリモデル)を参照してください。


aas volatileを宣言すると、2つの異なる読み取りが行われ、その間に変更が生じる可能性があります。
Holger、

6

これはすべてStephen Cによって十分に説明されています。おもしろいのは、次のJVMパラメータを使用して同じコードを実行してみることです。

-XX:InlineSmallCode=0

これにより、JIT(hotspot 7サーバーで行われる)による最適化が防止され、true永久に表示されます(2,000,000で停止しましたが、その後も続くと思います)。

詳細については、JITされたコードを以下に示します。正直に言うと、テストが実際に行われたかどうか、または2つの負荷がどこから来たかを知るほど流暢にアセンブリを読むことはできません。(26行目はテストflag = a != a、31行目はの右中括弧ですwhile(true))。

  # {method} 'run' '()V' in 'javaapplication27/TestThreadSafety$1'
  0x00000000027dcc80: int3   
  0x00000000027dcc81: data32 data32 nop WORD PTR [rax+rax*1+0x0]
  0x00000000027dcc8c: data32 data32 xchg ax,ax
  0x00000000027dcc90: mov    DWORD PTR [rsp-0x6000],eax
  0x00000000027dcc97: push   rbp
  0x00000000027dcc98: sub    rsp,0x40
  0x00000000027dcc9c: mov    rbx,QWORD PTR [rdx+0x8]
  0x00000000027dcca0: mov    rbp,QWORD PTR [rdx+0x18]
  0x00000000027dcca4: mov    rcx,rdx
  0x00000000027dcca7: movabs r10,0x6e1a7680
  0x00000000027dccb1: call   r10
  0x00000000027dccb4: test   rbp,rbp
  0x00000000027dccb7: je     0x00000000027dccdd
  0x00000000027dccb9: mov    r10d,DWORD PTR [rbp+0x8]
  0x00000000027dccbd: cmp    r10d,0xefc158f4    ;   {oop('javaapplication27/TestThreadSafety$1')}
  0x00000000027dccc4: jne    0x00000000027dccf1
  0x00000000027dccc6: test   rbp,rbp
  0x00000000027dccc9: je     0x00000000027dcce1
  0x00000000027dcccb: cmp    r12d,DWORD PTR [rbp+0xc]
  0x00000000027dcccf: je     0x00000000027dcce1  ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
  0x00000000027dccd1: add    rbx,0x1            ; OopMap{rbp=Oop off=85}
                                                ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
  0x00000000027dccd5: test   DWORD PTR [rip+0xfffffffffdb53325],eax        # 0x0000000000330000
                                                ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
                                                ;   {poll}
  0x00000000027dccdb: jmp    0x00000000027dccd1
  0x00000000027dccdd: xor    ebp,ebp
  0x00000000027dccdf: jmp    0x00000000027dccc6
  0x00000000027dcce1: mov    edx,0xffffff86
  0x00000000027dcce6: mov    QWORD PTR [rsp+0x20],rbx
  0x00000000027dcceb: call   0x00000000027a90a0  ; OopMap{rbp=Oop off=112}
                                                ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
                                                ;   {runtime_call}
  0x00000000027dccf0: int3   
  0x00000000027dccf1: mov    edx,0xffffffad
  0x00000000027dccf6: mov    QWORD PTR [rsp+0x20],rbx
  0x00000000027dccfb: call   0x00000000027a90a0  ; OopMap{rbp=Oop off=128}
                                                ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
                                                ;   {runtime_call}
  0x00000000027dcd00: int3                      ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
  0x00000000027dcd01: int3   

1
これは、無限ループがあり、多かれ少なかれすべてが巻き上げられる場合に、JVMが実際に生成する種類のコードの良い例です。ここでは、実際の「ループ」からの3つの命令です0x27dccd10x27dccdfjmp(ループが無限大であるため)ループでは無条件です。ループ内の他の2つの命令は、次の2つだけです。add rbc, 0x1これはインクリメントされますcountOfIterations(ループが終了しないため、この値は読み取られません。デバッガーで中断する場合に必要になる可能性があります)。 。
BeeOnRope

...そして、test実際にはメモリアクセスにのみ存在する奇妙な外観の命令(eaxメソッドで設定されることさえないことに注意してください!):これは、JVMがすべてのスレッドをトリガーするときに読み取り不可に設定される特別なページです。セーフポイントに到達するため、すべてのスレッドを既知の状態にする必要があるgcまたはその他の操作を実行できます。
BeeOnRope

さらに言えば、JVM instance. a != instance.aはループから完全に比較を引き上げ、ループに入る前に1回だけ実行しました。リロードする必要がないこと、instanceまたはavolatileと宣言されていないため、同じスレッドで変更できる他のコードがないため、ループ全体で同じであると見なされ、メモリで許可されています。モデル。
BeeOnRope

5

いいえ、a != aスレッドセーフではありません。この式は、load a、load、reform a、perform の3つの部分で構成されています!=。別のスレッドがaの親に固有のロックを取得しa、2つのロード操作の間の値を変更する可能性があります。

もう1つの要因aは、ローカルかどうかです。場合はaローカルで、その後、他のスレッドは、それへのアクセス権を持つべきではないので、スレッドセーフでなければなりません。

void method () {
    int a = 0;
    System.out.println(a != a);
}

また、常に印刷する必要がありますfalse

宣言aとしては、volatile場合のための問題解決にはならないでしょうaであるstaticか、インスタンスを。問題は、スレッドの値が異なることでaはなく、1つのスレッドが異なる値でa2回ロードされることです。これは、実際に作ることが少ない場合、スレッドセーフ.. Ifはaされていませんvolatileその後、aキャッシュされて、別のスレッドに変更がキャッシュされた値には影響しません。


あなたの例synchronizedは間違っています:そのコードが出力されることが保証されるためにはfalse設定 aするすべてのメソッドもでなければなりsynchronizedません。
ruakh 2013

なんでそうなの?メソッドが同期aされている場合、値の設定に必要な、メソッドの実行中に他のスレッドが親の組み込みロックをどのように取得するかa
DoubleMx2 2013

1
あなたの前提は間違っています。固有のロックを取得せずにオブジェクトのフィールドを設定できます。Javaでは、フィールドを設定する前に、スレッドがオブジェクトの固有のロックを取得する必要はありません。
ruakh 2013

3

奇妙な行動について:

変数aはとしてマークされていないため、volatileある時点で、その値がaスレッドによってキャッシュされる可能性があります。どちらaのSはa != aその後、キャッシュされたバージョンため、常に同じ(意味がありflag、常に今ありますfalse)。


0

単純な読み取りでもアトミックではありません。でaありlong、そのようにマークされていない場合volatile、32ビットJVM long b = aではスレッドセーフではありません。


揮発性と原子性は無関係です。ボラティリティをマークしたとしても、それは非アトミックになります
Narendra Pathai 2013

揮発性の長いフィールドの割り当ては常にアトミックです。++のような他の操作はそうではありません。
ZhekaKozlov、2013
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.