使用Volatile变量来安全发布不可变对象

4
我看到了这个陈述:
在正确构造的对象中,所有线程将看到最终字段的正确值,无论对象如何发布。
那么为什么需要使用volatile变量来安全地发布一个Immutable对象?
我真的很困惑。有人能用一个合适的例子来解释一下吗?
3个回答

5
在这种情况下,volatile 只能确保新对象的可见性;任何通过非 volatile 字段获取您的对象的其他线程都会根据 JSR-133 的初始化安全保证看到正确的 final 字段值。尽管如此,将变量设为 volatile 不会有害,从内存管理角度来看是正确的;对于在构造函数中初始化的非 final 字段,则确实需要使用 volatile(虽然不应该在不可变对象中存在这些字段)。如果您希望在线程之间共享变量,则需要确保足够的同步以提供可见性;尽管在这种情况下,您是正确的,构造函数的原子性不会受到威胁。感谢 Tom Hawtin 指出我完全忽略了 final 字段上的 JMM 保证;之前的错误答案如下所示。

volatile变量的原因是它在对象构造和变量赋值之间建立了一个happens-before关系(根据Java内存模型)。这可以实现两个目标:

  1. 来自不同线程的后续变量读取保证看到新值。如果没有将变量标记为volatile,则这些线程可能会看到引用的陈旧值。
  2. happens-before关系限制了编译器可以进行的重排序。如果没有volatile变量,则变量的赋值可能会在对象的构造函数运行之前发生,因此其他线程可能会在完全构造对象之前获取对该对象的引用。

由于不可变对象的基本规则之一是在构造函数期间不发布引用,因此可能正在引用第二点。在没有适当并发处理的多线程环境中,有可能在构建对象之前“发布”对象的引用。因此,另一个线程可以获取该对象,看到其字段之一为null,然后稍后看到这个“不可变”对象已更改。

请注意,如果您具有其他适当的同步原语,则无需使用volatile字段来实现此目的-例如,如果赋值(以及所有后续读取)在给定监视器上的同步块中完成-但在“独立”意义上,将变量标记为 volatile 是告诉JVM“这可能被多个线程读取,请在该上下文中使分配安全”的最简单方法。


1
JVM规范的final-field语义意味着,安全地发布(适当设计的)不可变对象是安全的。您可能有进一步的原因将引用分配给volatile字段,但如果没有它,您将看不到未初始化的final字段。 - Tom Hawtin - tackline
@Tom - 很好的观点,完全否定了我的答案。我会适当更新。 - Andrzej Doyle
这个线程如何保证安全?因为final字段是可变的,其他线程可能会看到不同的值,对吗? - amarnath harish

2
一个对不可变对象的易失引用可能会很有用。这将允许您交换一个对象以使新数据对其他线程可用。
但是,我建议您首先查看使用AtomicReference。
如果您需要最终易失字段,则存在问题。所有字段(包括final字段)在构造函数返回时立即对其他线程可用。因此,如果您在构造函数中将对象传递给另一个线程,那么该线程可能会看到不一致的状态。在我看来,您应该考虑使用不同的解决方案,以便无需这样做。

0

在Immutable类中,你无法真正看到差异。请看下面的例子,在Myclass.class中。

    public static Foo getInstance(){
    if(INSTANCE == null){
        INSTANCE = new Foo();
    }

    return INSTANCE;
}

在上述代码中,如果Foo被声明为final (final Foo INSTANCE;),它将确保不会在构造函数调用期间发布引用。部分对象构建是不可能的。
考虑这个例子...如果Myclass是不可变的,那么在对象构造之后它的状态就不会改变,使得Volatile关键字(volatile final Foo INSTANCE;)变得多余。但是如果这个类允许其对象状态被更改(非不可变),则多个线程可以实际上更新对象,并且一些更新对其他线程不可见,因此volatile关键字确保了非不可变类中对象的安全发布

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