Java中不可变对象的安全发布

Java中不可变对象的安全发布,java,concurrency,parallel-processing,java.util.concurrent,Java,Concurrency,Parallel Processing,Java.util.concurrent,我想了解发布不可变对象是否需要volatile 例如,假设我们有一个不可变的对象A: // class A is immutable class A { final int field1; final int field2; public A(int f1, int f2) { field1 = f1; field2 = f2; } } // class B publishes object of class A through a public filed

我想了解发布不可变对象是否需要
volatile

例如,假设我们有一个不可变的对象
A

// class A is immutable
class A {
  final int field1;
  final int field2;

  public A(int f1, int f2) {
    field1 = f1;
    field2 = f2;
  }
}
// class B publishes object of class A through a public filed
class B {
  private /* volatile? */ A toShare;

  // this getter might be called from different threads
  public A getA(){
    return toShare;
  }

  // this might be called from different threads
  public void setA(num1, num2) {
    toShare = new A(num1, num2);
  }
}
然后我们有一个从不同线程访问的类
B
。它包含对类
a
的对象的引用:

// class A is immutable
class A {
  final int field1;
  final int field2;

  public A(int f1, int f2) {
    field1 = f1;
    field2 = f2;
  }
}
// class B publishes object of class A through a public filed
class B {
  private /* volatile? */ A toShare;

  // this getter might be called from different threads
  public A getA(){
    return toShare;
  }

  // this might be called from different threads
  public void setA(num1, num2) {
    toShare = new A(num1, num2);
  }
}

据我所知,似乎不可变对象可以通过任何方式安全发布,那么这是否意味着我们不需要将
toShare
声明为
volatile
,以确保其内存可见性,您不能保证看到共享数据的
toShare
字段的所有更新。这是因为您的共享数据不使用任何同步构造来保证其可见性或可通过它跨线程访问的引用的可见性。这使得它可以在编译器和硬件级别进行大量优化

您可以安全地将
toShare
字段更改为引用
字符串
(该字符串在所有情况下都是不可变的),您可能(正确地)会对其更新可见性感到不安

您可以看到我创建的一个基本示例,它可以显示更新是如何丢失的,而无需采取任何额外措施来发布对不可变对象引用的更改。我在JDK 8u65和Intel®Core上使用了
-server
JVM标志运行它™ i5-2557M,忽略可能抛出的
NullPointerException
,并看到以下结果:

  • 如果
    safe
    volatile
    ,则第二个线程不会终止,因为它看不到第一个线程所做的许多更改
控制台输出:

  • safe
    更改为
    volatile
    时,第二个线程与第一个线程一起终止
控制台输出:


另外还有一个问题-如果
共享数据
(而不是
安全
)被设置为
易失性
,会发生什么?根据JMM会发生什么?

答案是,需要使用
volatile
或任何其他方式(例如,将
synchronized
关键字添加到签名获取和设置中)来生成发生/之前边缘。Final fields语义仅保证,如果有人看到指向类实例的指针,则所有Final字段在完成时都会根据构造函数设置其值:

这并没有说明引用本身的可见性。因为您的示例使用非final字段

私人托舍

您必须使用
volatile
synchronized
节或java.util.concurrent.locks.locks或AtomicReference等注意字段的可见性,以启动/保证缓存同步。顺便说一句,关于期末考试和安全出版的一些有用的东西


看来JMM应该考虑发布不可变对象的可见性问题,至少在实践中,并发中所说的是,3.5.2不可变对象和安全初始化

由于不可变对象非常重要,JavaMemory模型提供了初始化安全的特殊保证 用于共享不可变对象。正如我们所看到的,一个对象引用对于另一个线程来说是可见的,这并不意味着 必然意味着该对象的状态对消费线程可见。为了保证观点的一致性 对于对象的状态,需要同步

另一方面,即使不使用同步来发布数据,也可以安全地访问不可变对象 对象引用。为了保证初始化安全,必须满足所有不变性要求: 不可修改的状态,所有字段都是最终的,并且正确构造

任何线程都可以安全地使用不可变对象,而无需额外的同步,即使在需要同步时也是如此 不习惯发布它们

下一章3.5.3安全发布习惯用法指出,只有使用以下方法对非不变对象才需要安全发布:

  • 静态初始值设定项
  • 在volatile/final/AtomicReference中存储引用
  • 存储由锁保护的引用

  • 如果任何线程能够通过
    toShare
    检索对
    a
    对象的引用,则该
    a
    对象保证完全初始化。但是检索到的
    a
    是否是最新的值?因为其他线程可能会通过
    setA
    方法更新
    toShare
    。JVM是否保证更新的值不会在设置线程中本地缓存?感谢您的详细解释!为了使
    sharedData
    易失性,我最初的猜测是第二个线程永远不会终止。逻辑是,标记
    sharedData
    volatile
    将使读取
    sharedData
    的线程与上次写入它的线程的内存视图同步。但在本例中,它在构造期间只编写一次。由于线程1从未写入共享数据,因此线程2将永远不会与线程1的内存视图同步。然而,当我尝试它时,线程2确实正确终止。你能详细解释一下背后的原因吗?谢谢!不要将答案中不可或缺的代码放在外部站点上。把它包括在你的答案中。@VGR我认为代码是一个说明,不是我答案的一个组成部分。我把它放在这里主要是为了说明在很多情况下(当你需要存在量化或反驳普遍量化时),快速测试就足够了。如果我不认为这会使全文更难阅读的话,我仍然会包含代码。所以,虽然我大体上同意你的意见,但我会把你的建议转达给你