易失性变量能保证可变对象的安全发布吗?

11

阅读《Java并发编程实战》之后,我了解到:

要安全地发布一个对象,必须同时使其他线程看到对该对象的引用和状态。可以通过以下方法安全地构造对象:

  • 从静态初始化器中初始化对象引用
  • 将引用存储到volatile类型的字段或AtomicReference
  • 将引用存储到经过正确构造的对象的final字段中
  • 将引用存储到由锁保护的字段中

然而,第二种方式让我感到困惑。因为volatile只能保证另一个线程看到引用,但它没有同步所引用的对象的构造。那么,它如何保证可变对象已经被正确地构造?如果正在构造该对象的线程被另一个线程中断怎么办?

3个回答

13
我们需要展示构造一个对象并将其赋值给一个volatile变量发生在从该变量读取之前。 来自JLS第17章: 如果x和y是同一线程的操作,并且x在程序顺序中先于y,则hb(x,y)。
因此,从该线程的角度来看,对象的构造发生在将其分配给volatile变量之前。
如果一个动作x与后续动作y同步,则我们也有hb(x,y)。
并且:
如果hb(x,y)和hb(y,z),则hb(x,z)。
如果我们能够证明写入易失性变量(操作 y读取变量(操作 z同步,我们可以利用happens-before的传递性来证明构造对象(操作 x发生在读取对象之前。幸运的是:

对易失性变量v(§8.3.1.4)的写入任何线程后续读取v(其中“后续”根据同步顺序定义)同步

因此,我们可以看到通过这种方式发布时,一个正确构造的对象对任何线程都是可见的。

1
谢谢您的回答,它清楚地回答了我的问题。顺便说一下,据我所了解,“final”关键字也可以保证这一点,但为什么会这样呢?我猜机制背后是不同的? - Guifan Li
@GuifanLi 机制不同,详见JLS 17.5。但我认为关键部分是:“当构造函数完成时,对象被认为已经完全初始化。只能在该对象完全初始化后才能看到对该对象的引用的线程保证可以看到该对象的最终字段的正确初始化值。” 因此,请确保带有final字段的对象的构造函数不会泄漏对this的引用,并且在完全初始化之前,没有任何东西可以看到该对象。这提供了可见性。 - erickson
很好的解释。但我还有一个问题。程序顺序规则被定义为部分排序。在这种情况下,JVM仍然有权更改x和y出现的顺序。我们如何保证它不会发生。 - Ravindra Ranwala
1
@RavindraRanwala 这是部分排序,因为它仅限于向与其同步的其他线程呈现程序顺序的外观。在这种情况下,读取易失性变量与写入变量同步。任何可被此类读取器检测到的执行更改都是不允许的。 - erickson
@erickson 那就意味着,当写操作完成时,读取操作才会执行。因此,无论写操作的顺序是X -> Y还是Y -> X,读者看到的最终结果都是一致的。太好了,现在我明白了。非常感谢! - Ravindra Ranwala

2
对于所有声明为volatile的变量(包括longdouble变量),读取和写入都是原子性的。

结果:在其他保证之外,volatile还保证了变量始终从所有线程共享的内存中读取 - 如果值发布在volatile变量中,则必须在完全构建之前完成。
换句话说,如果未发布volatile值,则没有其他线程会知道它 - 最可能的是“正在进行构造”的结果可能驻留在CPU缓存中或JVM用作“我自己用于自己目的的空间;你这个微不足道的Java代码,不要问里面有什么,这不是你的事情”中的内存中。


如果变量是数组的引用呢?例如:volatile int[] arr = new int[5]。我知道arr会被安全地发布,但是发布之后,如果我让arr[0] = 100,这是线程安全的吗?我的意思是,这是否保证了arr[0] = 100的安全发布? - Guifan Li
据我所知/理解,在发布后,创建实例(或数组)的内容是没有保证的。虽然我可能是错的,但是现在有点忙,无法搜索答案。 - Adrian Colomitchi

1

volatile只能保证引用对另一个线程可见,但它并没有同步所引用的对象的构建。

是的,你说得对。你可以在下面的问题中找到有关volatile变量内部的更多细节:

Java中volatile和synchronized之间的区别

那么它如何保证可变对象已经正确构建,如果构建此对象的线程被另一个线程中断呢?

您必须使用其他编程结构来实现线程安全:使用synchronized结构或synchronized结构的替代方法。

请参阅下面相关的SE问题:

Java中避免使用synchronized(this)吗?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接