【问题标题】:Safe publication of immutable objects in JavaJava中不可变对象的安全发布
【发布时间】:2016-04-09 23:36:13
【问题描述】:

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

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

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

  public A(int f1, int f2) {
    field1 = f1;
    field2 = f2;
  }
}

然后我们有一个类B,可以从不同的线程访问。它持有对 A 类对象的引用:

// 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检索到A对象的引用,则保证A对象被完全初始化。
  • 但是检索到的A 会是最新的值吗?因为其他线程可能会通过setA 方法更新toShare。 JVM是否保证更新的值不会在设置线程本地缓存?

标签: java concurrency parallel-processing java.util.concurrent


【解决方案1】:

不,不能保证您会看到共享数据的 toShare 字段的所有更新。这是因为您的共享数据不使用任何同步结构来保证其可见性或跨线程可通过它访问的引用的可见性。这使得它可以在编译器和硬件级别进行大量优化。

您可以安全地将您的 toShare 字段更改为引用 String(对于您的所有目的来说也是不可变的),您可能(并且正确地)对它的更新可见性感到更加不安。

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

  • 如果safe 不是volatile,第二个线程不会终止,因为它看不到第一个线程所做的许多更改

控制台输出:

[T1] Shared data visible here is 2147483647
  • safe 更改为volatile 时,第二个线程与第一个线程一起终止

控制台输出:

[T1] Shared data visible here is 2147483647
[T2] Last read value here is 2147483646
[T2] Shared data visible here is 2147483647

附:还有一个问题——如果sharedData(而不是safe)被设为volatile,会发生什么?根据 JMM 会发生什么?

【讨论】:

  • 感谢详细的解释!为了使sharedData 不稳定,我最初的猜测是第二个线程永远不会终止。逻辑是标记sharedData volatile 将使读取sharedData 的线程同步到最后写入它的线程的内存视图。但在这个例子中,它只在构建过程中写入一次。由于线程 1 从未写过 sharedData,线程 2 将永远不会与线程 1 的内存视图同步。但是,当我尝试它时,线程 2 确实正确终止。您能详细说明其背后的原因吗?非常感谢!
  • 不要将与您的答案不可或缺的代码放在外部站点上。包括它你的答案。
  • @VGR 我认为代码只是一个插图,不是我答案的组成部分。我主要是为了表明在许多情况下(当您需要进行存在量化或反驳普遍量化时),快速测试就足够了。如果我不认为它会使整个文本更难阅读,我仍然会包含代码。因此,虽然我总体上同意你的看法,但我会转达你的建议。
  • @DimitarDimitrov 如果toShare 变量被声明为volatileA 类甚至不必被定义为不可变的,除非它的状态在构造之后不能被修改。 AFAIK 分配对声明为volatile 的变量的引用时,分配之前的所有写入都必须happen-before。这意味着A 类的对象在分配时已经正确构造。我说的对吗?
【解决方案2】:

答案是NO,需要使用volatile 或任何其他方式(例如,在get 和set 两个签名中添加synchronized 关键字)来制作Happens/Before 边。最终字段语义仅保证如果有人看到指向类实例的指针,则所有最终字段在完成时根据构造函数设置其值: http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.5

这并没有说明引用本身的可见性。由于您的示例使用非最终字段

私人A分享;

您必须注意 volatilesynchronized 部分或 java.util.concurrent.locks.Locks 或 AtomicReference 等字段的可见性,以启动/保证缓存同步。一些有用的东西,顺便说一句,关于决赛和安全发布http://shipilev.net/blog/2014/safe-public-construction/

http://shipilev.net/blog/2014/all-fields-are-final/

【讨论】:

  • 我认为Final fields semantic only guarantees that if someone sees a pointer to an instance of the class, all final fields have their values set according to constructor by the moment 仅适用于正确构造对象的情况。即不会在构造函数中泄漏对this 的引用。
  • yes - "根据构造函数设置完成"
【解决方案3】:

似乎 JMM 应该解决发布不可变对象的可见性问题,至少在实践中的并发,3.5.2 不可变对象和安全初始化中所说的:

因为不可变对象如此重要,JavaMemory 模型提供了初始化安全的特殊保证 用于共享不可变对象。正如我们所见,对象引用对另一个线程可见并不 必然意味着该对象的状态对消费线程是可见的。为了保证一致的视图 对象的状态,需要同步。

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

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

以下章节3.5.3 安全发布惯用语指出,只有对于非不可变对象才需要使用以下方法进行安全发布:

  1. 静态初始化器
  2. 在 volatile/final/AtomicReference 中存储引用
  3. 存储由锁保护的引用

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-01-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多