java - 如果 synchronized 创建一个 happen-before 关系并防止重新排序,为什么 DCL 需要 volatile

标签 java multithreading synchronization

我试图理解在双重检查锁定中对 volatile 的需求(我知道有比 DCL 更好的方法)我已经阅读了一些与我类似的问题,但似乎没有人能解释我在寻找什么。我什至在 SO 上找到了一些赞成的答案,这些答案说不需要 volatile(即使对象是可变的),但是,我读过的所有内容都不是这样说的。

我想知道的是,如果 synchronized 创建一个 happens-before relationship,为什么 volatile 在 DCL 中是必需的?和 prevents reordering

这是我对 DCL 工作原理的理解和一个​​ example :

// Does not work
class Foo {
  private Helper helper = null; // 1
  public Helper getHelper() { // 2
    if (helper == null) { // 3
      synchronized(this) { // 4
        if (helper == null) { // 5
          helper = new Helper(); // 6
        } // 7
      } // 8
    } // 9
  return helper; // 10
}

这不起作用,因为 Helper 对象不是 immutablevolatile 我们知道 volatile 导致每次写入都刷新到内存,每次读取都来自内存。这很重要,这样线程就不会看到陈旧的对象。

所以在我列出的示例中,线程 A 可能会在 第 6 行 开始初始化一个新的 Helper 对象。然后 线程 B 出现并在 第 3 行 看到一个半初始化的对象。 Thread B 然后跳转到第 10 行 并返回一个半初始化的 Helper 对象。

添加 volatile 通过 happens before 关系修复此问题,并且 JIT 编译器无法进行重新排序。所以 Helper 对象在完全构建之前不能写入 helper 引用(?,至少我认为这是告诉我的...)。

然而看完JSR-133 documentation ,我有点糊涂了。它指出

Synchronization ensures that memory writes by a thread before or during a synchronized block are made visible in a predictable manner to other threads which synchronize on the same monitor. After we exit a synchronized block, we release the monitor, which has the effect of flushing the cache to main memory, so that writes made by this thread can be visible to other threads. Before we can enter a synchronized block, we acquire the monitor, which has the effect of invalidating the local processor cache so that variables will be reloaded from main memory. We will then be able to see all of the writes made visible by the previous release.

所以 Java 中的 synchronized 创建了内存屏障和 happens before 关系。

所以 Action 被刷新到内存中,所以这让我质疑为什么变量需要 volatile

文档还指出

This means that any memory operations which were visible to a thread before exiting a synchronized block are visible to any thread after it enters a synchronized block protected by the same monitor, since all the memory operations happen before the release, and the release happens before the acquire.

我猜测为什么我们需要 volatile 关键字以及为什么 synchronize 是不够的,因为内存操作对其他线程不可见,直到 Thread A 退出同步块(synchronized block),线程 B 进入同一个锁上的同一个 block 。

线程 A 可能在第 6 行 初始化对象,而线程 B第 3 行> 在 第 8 行 Thread A 刷新之前。

然而,this SO answer似乎与同步块(synchronized block)阻止“从同步块(synchronized block)内部到外部”重新排序相矛盾

最佳答案

如果 helper 不为 null,如何确保代码将看到构造 helper 的所有效果?没有 volatile,什么都做不到。

考虑:

  synchronized(this) { // 4
    if (helper == null) { // 5
      helper = new Helper(); // 6
    } // 7

假设这在内部实现为首先将 helper 设置为非空值,然后调用构造函数来创建有效的 Helper 对象。没有规则可以阻止这种情况。

另一个线程可能会将 helper 视为非空,但构造函数甚至还没有运行,更不用说使其效果对另一个线程可见了。

在我们可以保证构造函数的所有结果对该线程可见之前,不允许任何其他线程看到 helper 设置为非空值是至关重要的。

顺便说一下,让这样的代码正确是非常困难的。更糟糕的是,它似乎在 100% 的时间内都可以正常工作,然后突然在不同的 JVM、CPU、库、平台或其他任何东西上崩溃。通常建议避免编写此类代码,除非证明需要满足性能要求。这种代码很难写,很难理解,很难维护,也很难正确。

关于java - 如果 synchronized 创建一个 happen-before 关系并防止重新排序,为什么 DCL 需要 volatile,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42101139/

相关文章:

java - 基于两列对大文件进行排序

java - 共享进程/线程

ajax - 在循环中与 JQuery 同步 Ajax 请求

java - Spring 启动: how to set Async timeout when deploying to an external server

java - java中如何获取字符串值

c# - 如何编写触发器?

.Net Threading - 线程是否会锁定单个锁定对象的所有同步块(synchronized block)

javascript - 如何让它等到一切完成直到下一次迭代

c# - 等到单元测试中的所有任务完成

java - 与Tomcat 7相互认证