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

使用Java指令重新排序,代码的执行顺序由JVM在编译时或运行时更改,可能导致不相关的语句按顺序执行。

编辑:[指令重新排序会产生违反直觉的结果。许多CPU架构可以对机器指令的内存交互进行重新排序,即使编译器没有改变指令顺序,也会导致类似的意外结果。因此,术语记忆重新排序可能比指令重新排序更适合。

所以我的问题是:

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

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

除了一个错误的行为示例之外,仅仅显示一个简单程序的组装中发生的实际重新排序也可能是很好的。


答案 1

这演示了某些分配的重新排序,在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;
    }

}

打印写入 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

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

编辑:由于写入按 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:在开始之前运行到完成。threadAthreadB
  • x = 1, y = 0:在开始之前运行到完成。threadBthreadA
  • x = 1, y = 1:它们的指令是交错的。

没有人可以预料到,正如测试结果所显示的那样,这种情况可能会发生。x = 0, y = 0

每个线程中的操作彼此没有数据流依赖性,因此可以无序执行。(即使它们是按顺序执行的,从 的角度来看,将缓存刷新到主内存的时间也可能使 中的赋值以相反的顺序发生。threadBthreadA

enter image description here Java Concurrency in Practice, Brian Goetz