web-dev-qa-db-ja.com

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

i++などの複合操作は、multiple操作を伴うため、スレッドセーフではないことを知っています。

しかし、参照自体をスレッドセーフな操作でチェックしていますか?

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
140
Narendra Pathai

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

_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への変更が現在のスレッドに見えるという保証はありません。

122

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

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

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

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

これは、a =がtrueを返す可能性があることを意味します。

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

または、a != aは、falseが同時に変更されていたとしても、aを返すことができました。


「奇妙な行動」について:

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

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

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

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

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

これはallであり、JLSがJavaの実装を許可していることと一致しています。


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

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

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

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

  • 単一のスレッドで実行される場合、および
  • (メモリモデルに従って)正しく同期する異なるスレッドによって実行された場合。

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

47
Stephen C

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);
  }

}

10000回の呼び出しで2回失敗します。 [〜#〜] no [〜#〜]、それは[〜#〜] not [〜#〜]スレッドセーフ

27

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

Java VM可能性があります:

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

最初のケースでは、別のスレッドが2つの読み取りの間に「a」の値を変更できます。

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

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

スレッドセーフの詳細については、 JSR 1Java Memory Model )を参照してください。

15

それはすべて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   
_
6
assylias

いいえ、a != aはスレッドセーフではありません。この式は、aをロードし、aを再度ロードし、!=を実行する3つの部分で構成されます。別のスレッドがaの親の組み込みロックを取得し、2つのロード操作の間にaの値を変更することが可能です。

ただし、別の要因はaがローカルかどうかです。 aがローカルの場合、他のスレッドはアクセスできないため、スレッドセーフである必要があります。

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

また、常にfalseを出力する必要があります。

avolatileとして宣言しても、astaticまたはインスタンスである場合の問題は解決しません。問題は、スレッドがaの異なる値を持つことではなく、1つのスレッドが異なる値でaを2回ロードすることです。実際には、ケースのスレッドセーフが低下する可能性があります。avolatileでない場合、aがキャッシュされ、別のスレッドの変更がキャッシュされた値に影響を与えません。

5
DoubleMx2

奇妙な行動について:

変数avolatileとしてマークされていないため、ある時点でaの値がスレッドによってキャッシュされる可能性があります。 a != aの両方のasはキャッシュされたバージョンであり、常に同じです(つまり、flagは常にfalseになります)。

3
Walter Laan

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

0
ZhekaKozlov