Java 读取的重新排序

Java 读取的重新排序,java,multithreading,Java,Multithreading,假设有两个线程没有同步,一个设置n=1另一个执行method() 在下列情况下,“读取”总是指字段n的读取 public class MyClass { public int n = 0; public void method() { System.out.println(n); //read 1 System.out.println(n); //read 2 } } 以下输出是否可能 1 0 答案是肯定的,因为即使read1发生在read2之前,read2也有

假设有两个线程没有同步,一个设置
n=1
另一个执行
method()

在下列情况下,“读取”总是指字段
n
的读取

public class MyClass
{
  public int n = 0;

  public void method() {
    System.out.println(n); //read 1
    System.out.println(n); //read 2
  }
}
以下输出是否可能

1
0
答案是肯定的,因为即使read1发生在read2之前,read2也有可能在read1之前重新排序,因为它不会改变线程内执行的语义


这个推理正确吗?

之前发生的事件并不意味着两个任意操作的顺序。更准确地说,之前发生的最重要的事情是在一致性之前先进行写操作读操作。值得注意的是,它告诉读操作可以观察到哪些写操作:最后一次写入发生在顺序之前,或者任何其他未按顺序写入的写操作发生在顺序之前(race)。请注意,在不违反该要求的情况下,两次连续读取可能会看到从不同(racy)写入中获得的不同值

例如,JLS 17.4.5规定:

应该注意的是,一段感情的出现先于一段感情 两个行动之间的差距并不一定意味着他们必须采取行动 在实现中按该顺序放置。如果重新排序产生 结果与合法执行一致,并不违法

数据竞争就像这样令人毛骨悚然:快速读取可以在每次读取时返回令人惊讶的数据,Java内存模型捕捉到了这一点。因此,更精确的答案是,生成(1,0)的执行没有违反Java内存模型约束(同步顺序一致性、同步顺序-程序顺序一致性、发生在一致性之前、因果关系要求),因此是允许的

实现方面:在硬件上,两个负载可以在不同的时间启动和/或到达内存子系统,而不管它们的“程序顺序”,因为它们是独立的;在编译器中,指令调度也可能忽略独立读取的程序顺序,以“反直觉”的顺序将负载暴露给硬件

如果希望在程序顺序中观察读取,则需要更强大的属性。JMM将该属性赋予同步操作(在您的示例中,使一个变量
volatile
将使其具有该属性),这将按照与程序顺序一致的总同步顺序绑定操作。在这种情况下,(1,0)将被禁止

a上的插图(有关注意事项,请参阅完整来源):

即使在不重新排序加载的x86上,也会产生(1,0),oops:


使
保持不变。一个
易失性将使(1,0)消失。

我们有4个动作,在图表之前发生以下动作:

+-------+     ?    +-------+
| n = 0 |   ---->  | n = 1 |
+-------+          +-------+
    |
    |?
    v
  +---+             +---+
  | n |     ---->   | n |
  +---+             +---+
因为您没有给出初始化n的代码,所以不知道n=0是否发生在n=1之前,以及n=0是否发生在第一次读取n之前

如果这些边不存在,(n=1,n,n=0,n)是顺序一致的执行顺序,并且输出10是很可能的

如果已知n=0发生在n=1之前,则不存在与输出1 0顺序一致的执行

然而,Java语言规范只保证在没有数据竞争的情况下,所有执行都是顺序一致的,而我们的程序不是这样。具体而言,该规范写道:

更具体地说,如果两个操作共享一个“发生在”关系,则它们不必按照该顺序出现在它们不共享“发生在”关系的任何代码中。例如,处于数据竞争中的一个线程中的写操作与另一个线程中的读操作可能会出现顺序错误

我们说,变量v的read r可以观察到w到v的写入,如果在执行跟踪的before偏序中:

  • r不是在w之前排序的(即,hb(r,w)不是这种情况),并且

  • 不存在对v的干涉写入w′(即,不向v写入w′,使得hb(w,w′)和hb(w′,r))

在我们的例子中,两次读取都可以同时观察0和1,因为没有中间写入


因此,据我所知,Java语言规范允许输出10。

是的,这是正确的。但是,不是编译器“重新排序”线程。编译器只是编译。我怀疑
1,0
是否可能。这似乎非常非常错误。看到1,0我会很惊讶。你真的看到这个输出了吗?你正在寻找的术语将确保指令的顺序是一个“内存屏障”。您永远不会看到1,0,因为JVM将插入一个内存屏障,以确保指令顺序的语义@罗兰看一下
System.out::println的实现
->将有一个
synchronized(this){…}
只有当一个变量被多个线程访问时才需要volatile。在本例中,调用方法()在一个线程中执行。因此,它们共享同一个线程本地缓存。两个println执行都可以看到n的每一个变化。OP的问题特别说明一个线程正在设置
n=1
,另一个线程正在读取
n
。在单线程的情况下,没有racy写入,线程被迫观察对
n
的最新写入(这可以说是“在”两次读取之前发生)。@AlekseyShipilev这是一个非常聪明的JC压力测试!如果它能成为世界的一部分,那就太酷了samples@Eugene:它是:
      [OK] o.o.j.t.volatiles.ReadAfterReadTest                                                                                                      
    (fork: #1, iteration #1, JVM args: [-server])
  Observed state   Occurrences              Expectation  Interpretation                                              
          [0, 0]    16,736,450               ACCEPTABLE  Doing both reads early.                                     
          [1, 1]   108,816,262               ACCEPTABLE  Doing both reads late.                                      
          [0, 1]         3,941               ACCEPTABLE  Doing first read early, not surprising.                     
          [1, 0]        84,477   ACCEPTABLE_INTERESTING  First read seen racy value early, and the s...
+-------+     ?    +-------+
| n = 0 |   ---->  | n = 1 |
+-------+          +-------+
    |
    |?
    v
  +---+             +---+
  | n |     ---->   | n |
  +---+             +---+