40

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

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

所以我的问题是:

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

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

除非有错误的行为示例,否则仅显示在简单程序的汇编中发生的实际重新排序也可能很好。

4

3 回答 3

12

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

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

编辑: 因为写入按 a、b、c 的顺序发生,而读取按相反的顺序 c、b、a 发生,除非对写入(或读取)重新排序,否则您永远不会看到无效状态。

单个 cpu(或内核)执行的写入对所有处理器都以相同的顺序可见,请参见例如这个答案,它指向英特尔系统编程指南第 3 卷第 8.2.2 节。

所有处理器以相同的顺序观察单个处理器的写入。

于 2018-10-10T20:31:21.693 回答
5

测试

我编写了一个JUnit 5测试来检查指令重新排序是否在两个线程终止后发生。

  • 如果没有发生指令重新排序,则测试必须通过。
  • 如果发生指令重新排序,则测试必须失败。

‌</p>

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 并发实践,Brian Goetz

于 2018-10-12T08:44:19.820 回答
-1

对于单线程执行,重新排序根本不是问题,因为 Java 内存模型 (JMM)(保证与写入相关的任何读取操作都是完全有序的)并且不会导致意外结果。

对于并发执行,规则是完全不同的,事情变得更难理解(即使提供了一个会引发更多问题的简单示例)。但即便如此,JMM 也用所有极端情况完全描述了这一点,因此,也禁止意外结果。一般来说,如果所有障碍都放置正确,则禁止。

为了更好地理解重新排序,我强烈推荐这个主题,里面有很多例子。

于 2018-10-13T15:23:28.277 回答