【问题标题】:Need of volatile keyword in case of DCL在 DCL 的情况下需要 volatile 关键字
【发布时间】:2018-09-25 05:12:30
【问题描述】:

我只是在实践中阅读并发。我开始知道有必要在字段的双重检查锁定机制中使用 volatile 关键字,否则线程可以读取非空对象的陈旧值。因为它有可能在不使用 volatile 关键字的情况下重新排序指令。因为该对象引用可以在调用构造函数之前分配给资源变量。所以线程可以看到部分构造的对象。

我对此有疑问。

我假设同步块也会限制编译器重新排序指令,那么为什么我们需要 volatile 关键字呢?

public class DoubleCheckedLocking {
    private static volatile Resource resource;
    public static Resource getInstance() {
        if (resource == null) {
            synchronized (DoubleCheckedLocking.class) {
                if (resource == null)
                    resource = new Resource();
            }
        }
        return resource;
    }
}

【问题讨论】:

标签: java multithreading concurrency volatile


【解决方案1】:

如果调用线程 (T1) 也从同步块(在同一个锁上)读取它,则 JMM 只保证线程 T1 将看到由另一个线程 T2 在同步块内创建的正确初始化的对象。

由于 T1 可以看到资源不为空,因此无需通过同步块就立即返回它,它可以获取一个对象但看不到其状态已正确初始化

使用 volatile 会带来这种保证,因为在 volatile 字段的写入和该 volatile 字段的读取之间存在发生前的关系。

【讨论】:

  • 我有一个疑问。 volatile 在这里所做的是停止编译器重新排序分配对象引用和构造函数执行的指令?我的意思是在将对象引用分配给 resource 引用变量之前会调用构造函数。是真的吗?
  • 基本上是es,除了一个重点:编译器与此无关。这发生在运行时。此外,您不应该考虑 volatile 强制运行时执行的操作。您应该考虑它提供的保证,无论该保证在运行时以何种方式实现。
【解决方案2】:

正如其他人所观察到的那样,在这种情况下,Volatile 是必要的,因为在首次访问资源时可能会发生数据竞争。不能保证,没有volatile,线程A,读取一个非空值,实际上会访问完全初始化的资源——如果它同时被构建在线程B中线程 A 尚未到达的同步部分。然后线程A 可以尝试使用半初始化的副本。

虽然working since JSR-133 (2004) 仍然不推荐使用volatile 进行双重检查锁定,因为它是not very readable and not as efficient as the recommended alternative

private static class LazyResourceHolder {
  public static Resource resource = new Resource();
}

...

public static Resource getInstance() {
  return LazyResourceHolder.something;
}

这是Initialize-On-Demand Holder Class的成语,根据上面的页面,

[...] 的线程安全性源于以下事实: 类初始化的一部分,例如静态初始化器,是 保证对使用该类的所有线程可见,并且它的 内部类未加载这一事实的延迟初始化 直到某个线程引用了它的某个字段或方法。

【讨论】:

  • 那么 volatile 是如何确保其他线程的,它会看到完全初始化的对象? volatile 表示写入发生在读取之前。第一个线程不可能将分配的内存分配给引用变量。将内存分配给引用变量是 writing 的情况。然后其他线程正在读取引用变量的值(读取案例)。但是你的构造函数仍然没有被调用。
  • @sdindiver 如果您不使用 volatile 可能会发生这种情况,而这正是 volatile (以及具有发生前发生关系的所有其他方式)保证不会发生的情况。当你有一个happens-before保证时,这意味着运行时不能应用违背语言语义的重新排序(正常顺序是:首先调用构造函数,然后将结果分配给变量)。
【解决方案3】:

这里其实不用volatile。使用volatile 意味着每次在线程方法中使用实例变量时,多个线程将不会优化内存读取,但要确保一次又一次地读取它。我故意使用volatile 的唯一一次是在我有停止指示器的线程中 (private volatile boolean stop = false;)

像在您的示例代码中那样创建单例是不必要的复杂,并且不会提供任何实际的速度改进。 JIT 编译器非常擅长线程锁定优化。

使用以下方法创建单例会更好:

public static synchronized Resource getInstance() {
    if (resource == null) {
        resource = new Resource();
    }
    return resource;
}

这对于人类来说更容易阅读和推断其逻辑。

另见Do you ever use the volatile keyword in Java?,其中volatile确实通常用于线程中的某些循环结束标志。

【讨论】:

  • 这里需要 volatile。
  • 通过双重检查,是的,但我坚持不需要做这样的双重检查。
  • 是的,但这无关紧要。问题是为什么在使用 DCL 时需要 volatile,而不是是否应该使用 DCL。
  • 由于 elliot frisch 添加为评论 DCL 已损坏,我认为最好将 OP 指向正确的方向。
  • DCL 已损坏如果您不使该字段易失。你的回答是:这里没有必要使用 volatile。所以把 OP 和所有未来的读者都指向了错误的方向。
最近更新 更多