繁体   English   中英

如果同步,则会创建事前发生的关系并防止重新排序,为什么DCL需要使用volatile

[英]If synchronized creates a happen-before relationship and prevents reordering why is volatile needed for DCL

我正在尝试了解双重检查锁定中对volatile的需求(尽管我知道有比DCL更好的方法)我已经阅读了一些类似于我的SO问题,但是似乎没有一个可以解释我的期望对于。 我什至在SO上找到了一些赞成的答案,这些答案说不需要volatile(即使对象是可变的),但是,我所读的所有内容都相反。

我想知道的是,如果同步创建了事前发生的关系防止重新排序,为什么在DCL中需要使用volatile?

这是我对DCL的工作原理的理解和一个示例

// 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对象不是immutable也不是volatile而且我们知道volatile会导致每次写入都刷新到内存中,并且每次读取都来自内存。 这很重要,因此任何线程都无法看到过时的对象。

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

添加volatile解决此问题,并且需要happens before关联happens before进行,并且JIT编译器无法重新排序。 因此,在完全构造Helper对象之前,不能将其写入辅助参考(?,至少这是我认为这告诉我的...)。

但是,在阅读了JSR-133文档之后 ,我变得有些困惑。 它指出

同步确保以可预测的方式使线程在同步块之前或期间对内存的写入对于在同一监视器上同步的其他线程可见。 退出同步块后,我们释放监视器,其作用是将缓存刷新到主内存,以便该线程进行的写入对于其他线程可见。 在进入同步块之前,我们需要获取监视器,该监视器具有使本地处理器缓存无效的作用,以便可以从主内存中重新加载变量。 然后,我们将能够看到先前版本中所有可见的写入。

因此,在Java中进行synchronized会造成内存障碍,并且会在关系发生之前发生。

因此,这些操作已刷新到内存中,因此使我提出疑问,为什么变量上需要使用volatile

该文档还指出

这意味着线程在退出同步块之前对线程可见的任何内存操作在进入由同一监视器保护的同步块之后对任何线程都是可见的,因为所有内存操作都发生在释放之前,而释放发生在释放之前获得。

我对我们为什么需要volatile关键字以及为什么synchronize不够的猜测是,因为直到Thread A退出同步块并且Thread B在同一锁上进入同一块,其他线程才看不到内存操作。

Thread A可能在第6行初始化对象, Thread B第3行出现,然后Thread A第8行进行刷新

但是, 这个SO答案似乎矛盾,因为同步块阻止了“从同步块内部到外部的重新排序”

如果helper不为null,那么如何确保代码可以看到构造helper的所有效果? 没有volatile ,什么也做不了。

考虑:

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

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

另一个线程可能将helper视为非null,但构造函数尚未运行,更不用说使其效果对另一个线程可见。

至关重要的是,除非我们可以保证该线程可以看到构造函数的所有结果,否则不允许其他任何线程将helper设置为非null值。

顺便说一句,要获得正确的代码非常困难。 更糟糕的是,它似乎可以在100%的时间内正常工作,然后突然在其他JVM,CPU,库,平台或任何其他设备上中断。 通常建议避免编写此类代码,除非证明满足性能要求是必需的。 这种代码很难编写,难以理解,难以维护且难以正确使用。

@David Schwartz的回答相当不错,但我不确定其中有一件事情说得好。

关于我们为什么需要volatile关键字以及为什么同步不够的猜测,是因为直到线程A退出同步块并且线程B在同一锁上进入同一块之前,其他线程才看不到内存操作。

实际上不是同一把锁,而是任何锁,因为锁带有内存屏障。 volatile与锁定无关,而是围绕内存障碍,而synchronized块既是锁又是内存障碍。 您需要使用volatile因为即使线程A已正确初始化了Helper实例并将其发布到helper字段,线程B 需要越过内存障碍以确保它能够看到对Helper所有更新。

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

对。 线程A可能会初始化Helper它到达synchronized块的末尾之前发布它。 没有什么可以阻止它的发生。 并且由于允许JVM对来自Helper构造函数的指令进行重新排序,直到以后,所以可以将其发布到helper字段,但不会完全初始化。 即使线程A确实到达了synchronized块的末尾,然后Helper被完全初始化,也没有任何东西可以确保线程B看到所有更新的内存。

但是,这个SO答案似乎矛盾,因为同步块阻止了“从同步块内部到外部的重新排序”

不,这个答案并不矛盾。 您会混淆仅线程A会发生什么以及其他线程会发生什么。 就线程A(和中央内存)而言,退出synchronized块可确保Helper的构造函数已完全完成并发布到helper字段。 但是,这意味着直到线程B(或其他线程) 越过内存屏障,它才有意义。 然后,它们也将使本地内存缓存无效,并查看所有更新。

这就是为什么必须使用volatile的原因。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM