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
同期がない場合、このコード
_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
への変更が現在のスレッドに見えるという保証はありません。
チェックは
a != a
スレッドセーフ?
a
が別のスレッドによって更新される可能性がある場合(適切な同期なし!)、いいえ。
これをプログラムして複数のスレッドを使用しようとしましたが、失敗しませんでした。私のマシンではレースをシミュレートできなかったと思います。
それは何の意味もありません!問題は、a
が別のスレッドによって更新される実行がJLSによってallowedである場合、コードはスレッドではないということです。 -安全。特定のマシンの特定のテストケースと特定のJava実装で競合状態を発生させることができないという事実は、他の状況で発生することを排除しません。
これは、a =が
true
を返す可能性があることを意味します。
はい、理論的には、特定の状況下で。
または、a != a
は、false
が同時に変更されていたとしても、a
を返すことができました。
「奇妙な行動」について:
プログラムがいくつかの反復の間に開始されると、出力フラグ値を取得します。つまり、同じ参照で参照!=チェックが失敗します。しかし、いくつかの反復の後、出力は定数値falseになり、プログラムを長時間実行しても、単一の真の出力は生成されません。
この「奇妙な」動作は、次の実行シナリオと一致しています。
プログラムがロードされ、JVMがバイトコードの解釈を開始します。 (javapの出力からわかるように)バイトコードは2つのロードを行うため、場合によっては(明らかに)競合状態の結果が表示されます。
しばらくすると、コードはJITコンパイラーによってコンパイルされます。 JITオプティマイザーは、同じメモリスロット(a
)の2つのロードが近くにあることに気付き、2番目のロードを最適化します。 (実際、テストを完全に最適化する可能性があります...)
これで、2つのロードがなくなったため、競合状態が発生しなくなりました。
これはallであり、JLSがJavaの実装を許可していることと一致しています。
@krissはこうコメントしました:
これは、CまたはC++プログラマーが「未定義の動作」(実装に依存)と呼ぶものであるように見えます。 JavaこのようなコーナーケースではいくつかのUBが存在する可能性があるようです。
Java Memory Model( JLS 17.4 で指定)は、あるスレッドが別のスレッドによって書き込まれたメモリ値を参照することが保証される一連の前提条件を指定します。別の変数によって書き込まれた変数を読み取り、それらの前提条件が満たされていない場合、いくつかの可能な実行が発生する可能性があります...そのうちのいくつかは正しくない可能性があります(アプリケーションの要件の観点から)。 set可能な振る舞い(つまり、「整形式の実行」のセット)が定義されていますが、どの振る舞いが発生するかはわかりません。
コンパイラーは、コードの最終的な効果が同じであれば、ロードの結合と並べ替え、保存(およびその他の処理)を実行できます。
ただし、コードが適切に同期しない場合(したがって、「前に発生する」関係が整形式の実行のセットを十分に制約しない場合)、コンパイラは「誤った」結果を与える方法でロードとストアを並べ替えることができます。 (しかし、それは本当にプログラムが間違っていると言っているだけです。)
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 [〜#〜]スレッドセーフ
いいえそうではありません。比較の場合、Java VMは、スタック上で比較する2つの値を入力し、比較命令を実行する必要があります(どちらかが「a」 )。
Java VM可能性があります:
false
に置き換えます最初のケースでは、別のスレッドが2つの読み取りの間に「a」の値を変更できます。
どの戦略が選択されるかは、JavaコンパイラとJavaランタイム(特にJITコンパイラ)によって異なります。プログラムのランタイム中に変更されることもあります。
変数へのアクセス方法を確認する場合は、volatile
(いわゆる「ハーフメモリバリア」)にするか、完全なメモリバリア(synchronized
)を追加する必要があります。いくつかのhgiherレベルAPI(たとえば、Juneed Ahasanが言及したAtomicInteger
)を使用することもできます。
スレッドセーフの詳細については、 JSR 1 ( Java Memory Model )を参照してください。
それはすべて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
_
いいえ、a != a
はスレッドセーフではありません。この式は、a
をロードし、a
を再度ロードし、!=
を実行する3つの部分で構成されます。別のスレッドがa
の親の組み込みロックを取得し、2つのロード操作の間にa
の値を変更することが可能です。
ただし、別の要因はa
がローカルかどうかです。 a
がローカルの場合、他のスレッドはアクセスできないため、スレッドセーフである必要があります。
void method () {
int a = 0;
System.out.println(a != a);
}
また、常にfalse
を出力する必要があります。
a
をvolatile
として宣言しても、a
がstatic
またはインスタンスである場合の問題は解決しません。問題は、スレッドがa
の異なる値を持つことではなく、1つのスレッドが異なる値でa
を2回ロードすることです。実際には、ケースのスレッドセーフが低下する可能性があります。a
がvolatile
でない場合、a
がキャッシュされ、別のスレッドの変更がキャッシュされた値に影響を与えません。
奇妙な行動について:
変数a
はvolatile
としてマークされていないため、ある時点でa
の値がスレッドによってキャッシュされる可能性があります。 a != a
の両方のa
sはキャッシュされたバージョンであり、常に同じです(つまり、flag
は常にfalse
になります)。
単純な読み取りでもアトミックではありません。 a
がlong
であり、volatile
としてマークされていない場合、32ビットJVMではlong b = a
はスレッドセーフではありません。