【问题标题】:Can a volatile variable that is never assigned null to ever contain null?一个永远不会被赋值为 null 的 volatile 变量可以包含 null 吗?
【发布时间】:2016-03-24 07:58:18
【问题描述】:

可以在以下概念性 Java 示例中:

public class X implements Runnable {
    public volatile Object x = new Object();

    @Runnable
    public void run() {
        for (;;) {
            Thread.sleep(1000);
            x = new Object();
        }
    }
}

x 曾经被其他线程读取为null

奖励:我是否需要将其声明为 volatile(我并不真正关心该值,只要将来某个时候它将是新分配的值并且永远不会为空)

【问题讨论】:

  • 没有得到你的问题。 x 在方法范围内吗?
  • @sidgate volatile 只能应用于字段。
  • 如果其他线程能够在读取字段初始值设定项之前读取变量,那么确定...
  • 不希望示例过于臃肿,但现在要让它膨胀。
  • 不!没有膨胀!永远不要膨胀!

标签: java multithreading volatile


【解决方案1】:

从技术上讲,是的。这就是原来ConcurrentHashMap's readUnderLock的主要原因。 javadoc 甚至解释了如何:

读取处于锁定状态的条目的值字段。如果值字段似乎为空,则调用。这只有在编译器碰巧使用其表分配重新排序 HashEntry 初始化时才有可能,这在内存模型下是合法的,但不知道是否会发生。

由于 HashEntry 的 value 是不稳定的,这种类型的重新排序在构造上是合法的。

故事的寓意是所有非最终初始化都可能与对象构造竞争。


编辑: @Nathan Hughes 提出了一个有效的问题:

@John:在 OP 的示例中,构造不会在可运行的线程被传递到启动之前发生吗?看起来这会在字段初始化之后强加一个happens-before障碍。

Doug Lea 有几个关于这个主题的问题,整个线程可以是read here。他回答了评论:

但问题是新的 C 实例是否必须在易失性存储之后发生分配给其他内存。

有了答案

对不起,我记错了为什么我把这个问题当作基本解决了: 除非 JVM 总是将内存预先归零(这通常不是一个好的选择),否则 即使没有显式初始化,volatile 字段也必须归零 在构造函数主体中,在发布前带有释放栅栏。 因此,即使在某些情况下 JMM 不 严格要求防止出版物重新排序的机制 在具有 volatile 字段的类的构造函数中,唯一好的 JVM 的实现选择是使用非易失性写入 带有尾随释放栅栏,或执行每个易失性写入 有完整的围栏。无论哪种方式,发布都不会重新排序。 不幸的是,程序员不能依靠规范来保证 至少在 JMM 修订之前。

并完成:

  • 即使 final 字段是专门针对 发布安全、易变的字段并不总是如此。

  • 出于各种实现原因,JVM 安排 volatile 字段无论如何都是发布安全的,至少在 我们知道的案例。

  • 实际上更新 JMM/JLS 以强制执行此操作并不容易 (我知道没有任何小的调整适用)。但现在是个好时机 正在考虑对 JDK9 进行完整修订。

  • 与此同时,进一步测试是有意义的 并验证 JVM 是否满足这个可能的未来规范。

【讨论】:

  • 那么hoi polloi(即我)的解决方案是什么?代替volatile,使用final AtomicReference?
  • final AtomicReference 肯定会解决您在使用 volatile 字段时遇到的问题。根据 DL 的回答和过去的讨论,我希望 JDK 9 将引入一个更新的 JMM,以防止这种类型的重新排序。他还提到了这种情况发生的可能性有多大。
【解决方案2】:

这取决于X 实例的发布方式。

假设x 发布不安全,例如。通过非volatile 字段

private X instance;
...
void someMethod() {
    instance = new X();
}

访问instance 字段的另一个线程被允许查看引用未初始化X 对象的引用值(即其构造函数尚未运行的位置)。在这种情况下,其字段x 的值为null

上面的例子翻译成

temporaryReferenceOnStack = new memory for X // a reference to the instance
temporaryReferenceOnStack.<init> // call constructor
instance = temporaryReferenceOnStack;

但该语言允许以下重新排序

temporaryReferenceOnStack = new memory for X // a reference to the instance
instance = temporaryReferenceOnStack;
temporaryReferenceOnStack.<init> // call constructor

或直接

instance = new memory for X // a reference to the instance
instance.<init> // call constructor

在这种情况下,允许线程在调用构造函数初始化引用对象之前查看instance 的值。

现在,在当前的 JVM 中发生这种情况的可能性有多大?呃,我想不出一个 MCVE。


奖励:我是否需要将其声明为 volatile(我并不真正关心 这个值,在未来的某个时候就足够了 新分配的值,从不为空)

安全地发布封闭对象。或者使用 final AtomicReference 字段,您 set

【讨论】:

    【解决方案3】:

    没有。 Java 内存模型保证您永远不会看到x 为空。 x 必须始终是分配给它的初始值,或某个后续值。

    这实际上适用于任何变量,而不仅仅是volatile。你所问的是所谓的“凭空而来的价值观”。参考文献Java Concurrency in Practice 详细讨论了这个概念。

    您的问题的另一部分“我是否需要将x 声明为易失性:”考虑到上下文,是的,它应该是volatilefinal. 任何一个都为@ 引用的对象提供安全发布987654328@。参考文献Safe Publication. 显然,x 如果是final,以后就不能改了。

    【讨论】:

    • 根据 JLS 8, 17.4.4 同步顺序:“对 volatile 变量 v(第 8.3.1.4 节)的写入与任何线程对 v 的所有后续读取同步(其中“后续”是根据同步顺序定义)。”似乎支持答案。
    • 反对者可能正在考虑“this-escape”,它使对象在构造完成之前可见。但是,此对象没有声明的 ctor,因此无法进行 this 转义。
    • 就我个人而言,我一直使用这种模式,虽然我使用的是final 而不是volatile。这是一个很好的模式,可惜几乎在所有情况下都有效。
    • @Markspace 我认为从技术上讲,问题本身的答案(与其中给出的代码相反)是:“是的,如果课程是 Serializable 并且该字段也标记为 @987654334 @ 而你刚刚反序列化了它”。然后,如果您对代码进行 grep,您将永远不会看到它被设置为 null,但在反序列化期间它将被设置为 null
    • 写入 volatile 字段可能会与构造竞争,因为我的回答指出 只有当编译器碰巧使用其表分配重新排序 HashEntry 初始化时才有可能,这在内存模型下是合法的,但从未发生过。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-04-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-05-18
    • 2021-09-11
    相关资源
    最近更新 更多