如何演示Java指令重新排序问题?

Gon*_*n I 37 java multithreading compiler-optimization instruction-reordering

使用Java指令重新排序时,JVM会在编译时或运行时更改代码的执行顺序,这可能导致无关的语句无序执行.

所以我的问题是:

有人可以提供示例Java程序/片段,它可靠地显示指令重新排序问题,这也不是由其他同步问题(例如缓存/可见性或非原子r/w)引起的,就像我在这样的演示中尝试失败一样在我之前的问题中)

为了强调,我不是在寻找理论重新排序问题的例子.我正在寻找的是通过查看正在运行的程序的错误或意外结果来实际演示它们的方法.

除了一个错误的行为示例,只显示在一个简单程序的程序集中发生的实际重新排序也可能是好的.

Fra*_*man 6

这表明某些任务的重新排序,在1M次迭代中通常有几行打印线.

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;
}
Run Code Online (Sandbox Code Playgroud)

}

为write lambda打印程序集得到此输出(以及其他..)

                                                ; {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
Run Code Online (Sandbox Code Playgroud)

我不确定为什么最后一个mov dword ptr [r12+r10*8+10h],1h没有用putfield b和第16行标记,但你可以看到交换的b和c分配(c在a之后).

编辑: 因为写入按顺序a,b,c发生,并且读取以相反的顺序c,b,a发生a,除非重新排序写入(或读取),否则不应该看到无效状态.

由单个cpu(或核心)执行的写入由所有处理器以相同的顺序可见,参见例如此答案,其指向" 英特尔系统编程指南"第3卷第8.2.2节.

所有处理器以相同的顺序观察由单个处理器写入的内容.


And*_*lko 5

测试

我编写了一个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);
    }

}
Run Code Online (Sandbox Code Playgroud)

结果

我运行了测试,直到它失败了几次。结果如下:

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.
Run Code Online (Sandbox Code Playgroud)

解释

我们期望的结果是

  • x = 0, y = 1:threadAthreadB开始之前运行到完成。
  • x = 1, y = 0:threadBthreadA开始之前运行到完成。
  • x = 1, y = 1: 他们的指令是交错的。

没有人能预料到x = 0, y = 0,正如测试结果所示,这可能会发生。

每个线程中的动作之间没有数据流依赖,因此可以乱序执行。(即使它们是按顺序执行的,从 的角度来看,将高速缓存刷新到主内存的时间可能会使threadB中的分配以threadA相反的顺序发生。)

在此处输入图片说明 Java 并发实践,Brian Goetz