我试图理解双重检查锁定中volatile
的必要性(我知道有比DCL更好的方法)我已经阅读了一些类似于我的SO问题但似乎没有解释我在寻找什么。我甚至在SO上找到了一些赞成的答案,表示不需要挥发性(即使对象是可变的)但是,我读过的所有内容都说不然。
我想知道的是,如果同步创建happens-before relationship和prevents reordering,DCL需要使用volatile吗?
以下是我对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对象不是immutable
或volatile
而且我们知道这一点
volatile
会将每个写入刷新到内存,并使每次读取都来自内存。这很重要,这样任何线程都不会看到过时的对象。
因此,在我列出的示例中,Thread A
可以在第6行处开始初始化新的Helper
对象。然后Thread B
出现并在第3行处看到一半初始化对象。 Thread B
然后跳转到第10行并返回一半初始化的Helper
对象。
添加volatile
以happens before
关系修复此问题,JIT编译器无法重新排序。所以Helper
对象在完全构造之前不能写入辅助引用(?,至少这是我认为它告诉我的......)。
然而,在阅读JSR-133 documentation之后,我变得有点困惑。它声明
同步确保线程在之前或之前写入内存 在同步块期间以可预测的方式可见 到同一监视器上同步的其他线程。我们退出后 同步块,我们释放监视器,具有的效果 将缓存刷新到主内存,以便由此线程进行写入 其他线程可以看到。在我们进入同步之前 阻止,我们获取监视器,它具有无效的效果 本地处理器缓存,以便从main重新加载变量 记忆。然后,我们将能够看到所有可见的写入 上一版本。
因此,Java中的synchronized
会产生内存障碍,并且会在关系之前发生。
因此,操作被刷新到内存中,所以它让我怀疑为什么变量需要volatile
。
文档还说明了
这意味着线程可见的任何内存操作 在退出同步块之前,任何线程都可以看到它 因为所有,所以进入受同一监视器保护的同步块 内存操作发生在发布和发布之前 在获得之前发生。
我猜我们为什么需要volatile
关键字以及为什么synchronize
是不够的,因为在Thread A
退出synchronized块之前,内存操作对其他线程不可见Thread B
在同一个锁上输入相同的块。
Thread A
可能正在初始化第6行处的对象,而Thread B
在第3行处出现之前有{ {1}}在第8行。
然而,this SO answer似乎与此相矛盾,因为同步块阻止了“从同步块内部到其外部”的重新排序
答案 0 :(得分:3)
如果帮助器不为null,那么什么可以确保代码能够看到辅助器构造的所有效果?没有volatile
,什么都不会。
考虑:
synchronized(this) { // 4
if (helper == null) { // 5
helper = new Helper(); // 6
} // 7
假设在内部将其实现为首先将helper
设置为非空值,然后调用构造函数以创建有效的Helper
对象。没有规则可以阻止这种情况。
另一个线程可能会将helper
看作非空,但构造函数甚至还没有运行,更不用说它的效果对另一个线程可见了。
至关重要的是,在我们能够保证构造函数的所有后果对该线程可见之前,不允许任何其他线程将helper
设置为非空值。
顺便说一句,获得这样的代码是非常困难的。更糟糕的是,它似乎可以在100%的时间内正常工作,然后突然在不同的JVM,CPU,库,平台或其他任何东西上中断。通常建议避免编写此类代码,除非证明需要满足性能要求。这种代码难以编写,难以理解,难以维护,难以正确使用。
答案 1 :(得分:1)
@David Schwartz的回答非常好,但有一件事我不确定陈述得好。
我猜测为什么我们需要volatile关键字以及为什么同步是不够的,因为在线程A退出同步块并且线程B在同一个锁上进入同一个块之前,内存操作对其他线程不可见。
实际上不是同一个锁而是任何锁,因为锁具有内存屏障。 volatile
不是关于锁定,而是围绕内存障碍,而synchronized
块是锁和内存障碍。您需要volatile
,因为即使线程A已正确初始化Helper
实例并将其发布到helper
字段,线程B也需要也跨越内存屏障确保它看到Helper
的所有更新。
因此,在我列出的示例中,线程A可以在第6行开始初始化一个新的Helper对象。然后线程B出现并在第3行看到一个半初始化对象。然后线程B跳转到第10行并返回一半初始化的Helper对象。
右。线程A可能会初始化Helper
并在它到达synchronized
块结束之前将其发布到。没有什么可以阻止它发生。并且因为允许JVM重新排序来自Helper
构造函数的指令,直到稍后,它可以发布到helper
字段但不会被填充初始化。即使线程A确实到达synchronized
块的末尾并且Helper
然后完全初始化,仍然没有任何东西可以确保线程B看到所有更新的内存。
但是,这个SO答案似乎与同步块相反,因为同步块阻止了“从同步块内部到其外部”的重新排序
不,这个答案并不矛盾。你混淆了线程A会发生什么,以及其他线程会发生什么。就线程A(和中央内存)而言,退出synchronized
块可确保Helper
的构造函数已完全完成并发布到helper
字段。但是,在线程B(或其他线程)也跨越内存屏障之前,这意味着什么。然后它们也将使本地内存缓存无效并查看所有更新。
这就是volatile
是必要的原因。