Java命令を使用すると、コードの実行順序を並べ替える命令がコンパイル時または実行時にJVMによって変更されるため、無関係なステートメントが順不同で実行される可能性があります。
だから私の質問は:
誰かが例を提供できますかJavaプログラム/スニペット、命令の並べ替えの問題を確実に示します。これは、他の同期の問題(キャッシュ/可視性や非アトミックr/wなど)によっても引き起こされません。 前の質問 でのそのようなデモでの失敗した試みのように)
強調するために、私は理論的な並べ替えの問題の例を探していません。私が探しているのは、実行中のプログラムの誤った結果または予期しない結果を見て、実際にそれらを実証する方法です。
欠陥のある動作の例を除いて、単純なプログラムのアセンブリで発生する実際の並べ替えを示すだけでもいいでしょう。
これは、特定の割り当ての並べ替えを示しています。100万回の反復のうち、通常、印刷された行がいくつかあります。
public class App {
public static void main(String[] args) {
for (int i = 0; i < 1000_000; i++) {
final State state = new State();
// a = 0, b = 0, c = 0
// Write values
new Thread(() -> {
state.a = 1;
// a = 1, b = 0, c = 0
state.b = 1;
// a = 1, b = 1, c = 0
state.c = state.a + 1;
// a = 1, b = 1, c = 2
}).start();
// Read values - this should never happen, right?
new Thread(() -> {
// copy in reverse order so if we see some invalid state we know this is caused by reordering and not by a race condition in reads/writes
// we don't know if the reordered statements are the writes or reads (we will se it is writes later)
int tmpC = state.c;
int tmpB = state.b;
int tmpA = state.a;
if (tmpB == 1 && tmpA == 0) {
System.out.println("Hey wtf!! b == 1 && a == 0");
}
if (tmpC == 2 && tmpB == 0) {
System.out.println("Hey wtf!! c == 2 && b == 0");
}
if (tmpC == 2 && tmpA == 0) {
System.out.println("Hey wtf!! c == 2 && a == 0");
}
}).start();
}
System.out.println("done");
}
static class State {
int a = 0;
int b = 0;
int c = 0;
}
}
書き込みラムダのアセンブリを印刷すると、この出力が(特に)取得されます。
; {metadata('com/example/App$$Lambda$1')}
0x00007f73b51a0100: 752b jne 7f73b51a012dh
;*invokeinterface run
; - Java.lang.Thread::run@11 (line 748)
0x00007f73b51a0102: 458b530c mov r10d,dword ptr [r11+0ch]
;*getfield arg$1
; - com.example.App$$Lambda$1/1831932724::run@1
; - Java.lang.Thread::run@-1 (line 747)
0x00007f73b51a0106: 43c744d41402000000 mov dword ptr [r12+r10*8+14h],2h
;*putfield c
; - com.example.App::lambda$main$0@17 (line 18)
; - com.example.App$$Lambda$1/1831932724::run@4
; - Java.lang.Thread::run@-1 (line 747)
; implicit exception: dispatches to 0x00007f73b51a01b5
0x00007f73b51a010f: 43c744d40c01000000 mov dword ptr [r12+r10*8+0ch],1h
;*putfield a
; - com.example.App::lambda$main$0@2 (line 14)
; - com.example.App$$Lambda$1/1831932724::run@4
; - Java.lang.Thread::run@-1 (line 747)
0x00007f73b51a0118: 43c744d41001000000 mov dword ptr [r12+r10*8+10h],1h
;*synchronization entry
; - Java.lang.Thread::run@-1 (line 747)
0x00007f73b51a0121: 4883c420 add rsp,20h
0x00007f73b51a0125: 5d pop rbp
0x00007f73b51a0126: 8505d41eb016 test dword ptr [7f73cbca2000h],eax
; {poll_return}
0x00007f73b51a012c: c3 ret
0x00007f73b51a012d: 4181f885f900f8 cmp r8d,0f800f985h
なぜ最後のmov dword ptr [r12+r10*8+10h],1h
はputfield bと行16でマークされていませんが、bとc(aの直後のc)の交換された割り当てを確認できます。
EDIT:書き込みはa、b、cの順序で行われ、読み取りは逆の順序c、b、aで行われるため、書き込み(または読み取り)は並べ替えられます。
単一のCPU(またはコア)によって実行された書き込みは、すべてのプロセッサで同じ順序で表示されます。 この回答 、これは インテルシステムプログラミングガイド 第3巻のセクション8.2.2を指します。
単一のプロセッサによる書き込みは、すべてのプロセッサで同じ順序で観察されます。
2つのスレッドの終了後に命令の並べ替えが行われたかどうかをチェックする JUnit 5 テストを作成しました。
public class InstructionReorderingTest {
static int x, y, a, b;
@org.junit.jupiter.api.BeforeEach
public void init() {
x = y = a = b = 0;
}
@org.junit.jupiter.api.Test
public void test() throws InterruptedException {
Thread threadA = new Thread(() -> {
a = 1;
x = b;
});
Thread threadB = new Thread(() -> {
b = 1;
y = a;
});
threadA.start();
threadB.start();
threadA.join();
threadB.join();
org.junit.jupiter.api.Assertions.assertFalse(x == 0 && y == 0);
}
}
私はテストを実行しました 失敗するまで 数回。結果は次のとおりです。
InstructionReorderingTest.test [*] (12s 222ms): 29144 total, 1 failed, 29143 passed.
InstructionReorderingTest.test [*] (26s 678ms): 69513 total, 1 failed, 69512 passed.
InstructionReorderingTest.test [*] (12s 161ms): 27878 total, 1 failed, 27877 passed.
私たちが期待する結果は
x = 0, y = 1
:threadA
は、threadB
が開始する前に完了まで実行されます。x = 1, y = 0
:threadB
は、threadA
が開始する前に完了まで実行されます。x = 1, y = 1
:それらの命令はインターリーブされます。誰も期待できないx = 0, y = 0
、テスト結果が示すように発生する可能性があります。
各スレッドのアクションは、互いにデータフローに依存しないため、順不同で実行できます。 (順番に実行された場合でも、キャッシュがメインメモリにフラッシュされるタイミングにより、
threadB
の観点から、threadA
の割り当てが逆の順序で発生したように見えることがあります。)
シングルスレッド実行の場合、Javaメモリモデル(JMM)(書き込みに関連する読み取りアクションがすべて順序付けられていることを保証する)のため、並べ替えはまったく問題にならず、予期しない結果につながることはありません。
同時実行の場合、ルールは完全に異なり、物事を理解するのがより複雑になります(さらに多くの問題を提起する単純な例を提供しても)。しかし、これもJMMによってすべてのコーナーケースで完全に記述されているため、予期しない結果も禁止されています。一般に、すべてのバリアが正しく配置されている場合は禁止されています。
並べ替えの理解を深めるために、 this の件名に多くの例を含むことを強くお勧めします。