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

时间:2018-10-04 14:06:39

标签: java multithreading compiler-optimization instruction-reordering

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

所以我的问题是:

有人可以提供一个示例Java程序/代码段,该程序可靠地显示指令重新排序问题,该问题也不是由其他同步问题(例如缓存/可见性或非原子读/写)引起的,例如我在尝试失败时遇到的问题my previous question中的演示)

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

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

3 个答案:

答案 0 :(得分:5)

这演示了某些分配的重新排序,在1M迭代中,通常会有几条印刷线。

import scala.concurrent.{Future, Promise}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.language.implicitConversion
import scala.util.{Failure, Success}

implicit def completableFutureToFuture[T](cf: CompletableFuture[T]): Future[T] = {
  val p = Promise[T]() // Promise monitoring result of java cf.

  // Handle completion of Java future.
  cf.whenComplete {(result, error) =>

    // If error is not null, then future failed.
    if(error ne null) p.failure(error)

    // Otherwise, it succeeded with result.
    else p.success(result)
  }

  // Return the Scala future associated with the promise.
  p.future
}

}

打印用于写lambda的程序集将获得此输出(以及其他信息。)

scala> val scalaFuture = future // Implicit conversion called.
scalaFuture: scala.concurrent.Future[String] = Future(Success(We done did it!))

scala> scalaF.onComplete {
     |   case Success(s) => println(s"Succeeded with '$s'")
     |   case Failure(e) => println(s"Failed with '$e'...")
     | }

我不确定为什么最后一个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; } 没有用putfield b和第16行标记,但是您可以看到b和c的互换分配(c在a之后)。

编辑: 由于写操作按a,b,c顺序发生,而读操作按相反的c,b,a顺序发生,因此除非重新定义写(或读)顺序,否则您永远不会看到无效状态。

由单个CPU(或内核)执行的写入操作对于所有处理器而言都是相同的顺序,例如参见this answer,指向Intel System Programming Guide第3卷第8.2.2节。

  

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

答案 1 :(得分: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);
    }

}

结果

我进行了until it fails次测试。结果如下:

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 = 1threadAthreadB开始之前运行完成。
  • x = 1, y = 0threadBthreadA开始之前运行完成。
  • x = 1, y = 1:他们的指示是交错的。

没有人期望x = 0, y = 0,这可能会随着测试结果的显示而发生。

  

每个线程中的动作彼此之间没有数据流依赖性,因此可以无序执行。 (即使按顺序执行,从threadB的角度看,将缓存刷新到主存储器的时间也会使threadA中的分配看起来是相反的顺序。)

     

enter image description here   实践中的Java并发,Brian Goetz

答案 2 :(得分:0)

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

对于并发执行,规则是完全不同的,并且事情变得更加复杂(即使提供简单的示例,也会引发更多的问题)。但是即使JMM在所有极端情况下也完全描述了这一点,所以意外的结果也被禁止。通常,如果所有障碍都放置正确,则禁止使用。

为了更好地理解重新排序,我强烈建议this主题,里面有很多示例。