Java 构造函数与指令重新排序

Java 构造函数与指令重新排序,java,thread-safety,Java,Thread Safety,我只是偶然发现了一个我以前从未听说过,在其他地方也找不到的说法。这种说法是,从另一个线程的角度来看,构造函数返回的值的赋值可以根据构造函数内部的指令重新排序。换句话说,声明是在下面的代码中,另一个线程可以读取未设置x值的非空值a class MyInt { private int x; public MyInt(int value) { x = value; } public int getValue() { return x; } }

我只是偶然发现了一个我以前从未听说过,在其他地方也找不到的说法。这种说法是,从另一个线程的角度来看,构造函数返回的值的赋值可以根据构造函数内部的指令重新排序。换句话说,声明是在下面的代码中,另一个线程可以读取未设置
x
值的非空值
a

class MyInt {
   private int x;

   public MyInt(int value) {
      x = value;
   }

   public int getValue() {
      return x;
   }
}

MyInt a = new MyInt(42);
这是真的吗

编辑:

我认为可以保证,从执行
MyInt a=new MyInt(42)
的线程的角度来看,
x
的赋值与
a
的赋值具有先发生后发生的关系。但这两个值都可以缓存在寄存器中,并且它们可能不会按照最初写入的顺序刷新到主内存中。因此,如果没有内存屏障,另一个线程可以在写入
x
的值之前读取
a
的值。对吗

那么,根据您的回答和下面的评论,这些对线程安全性的评估是正确的吗

// thread-safe
class Foo() {
   final int[] x;

   public Foo() {
      int[] tmp = new int[1];
      tmp[0] = 42;
      x = tmp; // memory barrier here
   }
}

// not thread-safe
class Bar() {
   final int[] x = new int[1]; // memory barrier here

   public Bar() {
      x[0] = 42; // assignment may not be seen by other threads
   }
}

如果这是正确的。。。哇,这真的很微妙。

在Java内存模型的意义上-是的。然而,这并不意味着你会在实践中观察到它

从以下角度来看:可能导致可见重新排序的优化不仅可能发生在编译器中,也可能发生在CPU中。但是CPU对对象及其构造函数一无所知,对于处理器来说,这只是一对赋值,如果CPU的内存模型允许,可以对其重新排序

当然,编译器和JVM可能会指示CPU不要通过放入生成的代码来重新排序这些分配,但对所有对象这样做会破坏CPU的性能,CPU可能严重依赖于这种激进的优化。这就是为什么Java内存模型不为这种情况提供任何特殊保证的原因

例如,这导致了Java内存模型下的一个众所周知的缺陷

换句话说,声明是在下面的代码中,另一个线程可以读取a的非空值,其中x的值尚未设置

class MyInt {
   private int x;

   public MyInt(int value) {
      x = value;
   }

   public int getValue() {
      return x;
   }
}

MyInt a = new MyInt(42);
简而言之,答案是肯定的

详细回答:另一个线程读取非空
a
且值
x
尚未设置的非空
的关键点不是严格的指令重新排序,而是处理器缓存其寄存器中的值(和L1缓存),而不是从主存中读取这些值。这可能间接意味着重新排序,但这不是必需的


虽然CPU寄存器中的值缓存有助于加快处理速度,但它引入了在不同CPU上运行的不同线程之间的值可见性问题。如果总是从主程序区域读取值,则所有线程都会一致地看到相同的值(因为该值有一个副本)。在您的示例代码中,如果成员字段
x
的值被缓存到由线程1访问的CPU1寄存器中,并且CPU-2上运行的另一个线程线程2现在从主内存读取并更新该值,那么从程序的角度来看,缓存在CPU-1(由线程1处理)中的该值现在无效,但是Java规范本身允许虚拟机将此视为有效的场景。

您引用的文章在概念上是正确的。它的术语和用法有点不精确,正如你的问题一样,这会导致潜在的沟通错误和误解。我似乎在这里反复强调术语,但是Java内存模型非常微妙,如果术语不精确,那么理解就会受到影响

我将从你的问题(和评论)中摘录要点,并提供答案

构造函数返回的值的赋值可以根据构造函数内部的指令重新排序

几乎是的。。。可以重新排序的不是指令,而是内存操作(读和写)。一个线程可以以特定的顺序执行两条写指令,但是数据到达内存,以及这些写入到其他线程的可见性,可能以不同的顺序发生

我认为可以保证,从执行
MyInt a=new MyInt(42)
的线程的角度来看,
x
的赋值与
a
的赋值具有先发生后发生的关系

同样,几乎是。确实,按照程序顺序,对
x
的赋值发生在对
a
的赋值之前。然而,before是一个适用于所有线程的全局属性,所以谈论before对于特定线程是没有意义的

但这两个值都可以缓存在寄存器中,并且它们可能不会按照最初写入的顺序刷新到主内存中。因此,如果没有内存屏障,另一个线程可以在写入x之前读取a的值

再一次,几乎是。值可以缓存在寄存器中,但部分内存硬件(如缓存或写缓冲区)也可能导致重新排序。硬件可以使用各种机制来更改顺序,例如缓存刷新或内存屏障(通常不会导致刷新,但只会防止某些重新排序)。然而,从硬件角度考虑这一点的困难在于,实际系统相当复杂,并且具有不同的行为。例如,大多数CPU都有几种不同的记忆障碍。如果您想对JMM进行推理,您应该考虑模型的元素:内存操作和同步,它们通过建立before关系来约束重新排序

因此,为了从JMM的角度重新审视这个示例,我们看到了对字段
x
的写入和