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

如何演示Java指令重新排序问题?,java,multithreading,compiler-optimization,instruction-reordering,Java,Multithreading,Compiler Optimization,Instruction Reordering,使用Java指令重新排序,JVM在编译时或运行时会更改代码的执行顺序,这可能会导致不相关的语句无序执行 所以我的问题是: 有人能提供一个示例Java程序/代码段,它可靠地显示了指令重新排序问题,而这不是由其他同步问题引起的(例如缓存/可见性或非原子r/w,如我在中的演示中失败的尝试) 要强调的是,我不是在寻找理论重新排序问题的例子。我所寻找的是一种通过看到运行程序的错误或意外结果来实际演示它们的方法 除了一个错误行为的例子,仅仅显示在一个简单程序的汇编中发生的实际重新排序也很好 这演示了某些作业

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

所以我的问题是:

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

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


除了一个错误行为的例子,仅仅显示在一个简单程序的汇编中发生的实际重新排序也很好

这演示了某些作业的重新排序,在1百万次迭代中,通常会有几行打印出来

公共类应用程序{
公共静态void main(字符串[]args){
对于(int i=0;i<1000_000;i++){
最终状态=新状态();
//a=0,b=0,c=0
//写入值
新线程(()->{
状态a=1;
//a=1,b=0,c=0
状态b=1;
//a=1,b=1,c=0
状态c=状态a+1;
//a=1,b=1,c=2
}).start();
//读取值-这永远不会发生,对吗?
新线程(()->{
//以相反的顺序复制,所以如果我们看到一些无效状态,我们知道这是由重新排序引起的,而不是由读/写中的竞争条件引起的
//我们不知道重新排序的语句是写语句还是读语句(稍后我们将确定它是写语句)
int tmpC=state.c;
int tmpB=state.b;
int tmpA=状态a;
如果(tmpB==1&&tmpA==0){
System.out.println(“嘿,wtf!!b==1&&a==0”);
}
如果(tmpC==2&&tmpB==0){
System.out.println(“嘿,wtf!!c==2&&b==0”);
}
如果(tmpC==2&&tmpA==0){
System.out.println(“嘿,wtf!!c==2&&a==0”);
}
}).start();
}
系统输出打印项次(“完成”);
}
静态类状态{
int a=0;
int b=0;
int c=0;
}
}
打印写入lambda的程序集将获得此输出(以及其他..)

我不知道为什么最后一个
mov dword ptr[r12+r10*8+10h],1h
没有用putfield b和第16行标记,但您可以看到b和c的交换赋值(a后面的c)

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

由单个cpu(或核心)执行的写入操作在所有处理器中以相同的顺序可见,例如,请参见第3卷第8.2.2节

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

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

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

结果 我做了几次测试。结果如下:

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


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

对于并发执行,规则是完全不同的,事情变得更加复杂,难以理解(即使通过提供简单的示例,也会引起更多的问题)。但是,即使这完全是由JMM在所有角落案例中描述的,因此,意外结果也被禁止。通常,如果所有障碍物都放置正确,则禁止使用


为了更好地理解重新排序,我强烈推荐包含大量内部示例的主题。

这不会是一个bug。如果没有适当的同步/内存屏障,则可以从其他线程看到指令重新排序。在
x86
上很难显示IMO,但这是一个非常好的解决方案question@Thilo我认为OP想要一个例子,不正确的同步触发了一个问题,特别是由指令重新排序引起的,这不是由读取的非原子性或其他一些同步问题引起的。有许多具体原因导致不正确的同步可能是一个问题;他们对这个特别的问题感兴趣。一个好的答案可能会显示带有重新排序指令的字节码,这是错误的,但实际上,如果指令按其原始顺序排列,则会起作用。更困难的演示可以检查JIT的本机代码输出,并指出它执行的导致失败的重新排序优化
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.