Java中同步的可见性效果
Posted
技术标签:
【中文标题】Java中同步的可见性效果【英文标题】:Visibility effects of synchronization in Java 【发布时间】:2019-03-19 04:33:30 【问题描述】:This 文章说:
在这个不兼容的代码示例中,Helper 类是不可变的 通过声明其字段最终。 JMM 保证不可变 对象在它们对任何其他人可见之前已完全构建 线。 getHelper() 方法中的块同步保证 可以看到辅助字段的非空值的所有线程 还将看到完全初始化的 Helper 对象。
public final class Helper
private final int n;
public Helper(int n)
this.n = n;
// Other fields and methods, all fields are final
final class Foo
private Helper helper = null;
public Helper getHelper()
if (helper == null) // First read of helper
synchronized (this)
if (helper == null) // Second read of helper
helper = new Helper(42);
return helper; // Third read of helper
但是,此代码不能保证在所有 Java Virtual 上都能成功 机器平台,因为没有发生之前的关系 在助手的第一次阅读和第三次阅读之间。因此,它是 帮助器的第三次读取可能会获得陈旧的空值 (可能是因为它的值被编译器缓存或重新排序), 导致 getHelper() 方法返回一个空指针。
我不知道该怎么做。我同意在第一次和第三次阅读之间没有发生关系,至少没有立即关系。从某种意义上说,第一次读取必须发生在第二次之前,并且第二次读取必须发生在第三次之前,因此第一次读取必须在第三次之前发生,是否存在传递前发生关系?
谁能更熟练地详细说明?
【问题讨论】:
This 基本上是多线程。没有保证。 【参考方案1】:不,这些读取之间没有任何传递关系。 synchornized
只保证在同一个锁的同步块中所做的更改的可见性。在这种情况下,所有读取都不会使用同一个锁上的同步块,因此这是有缺陷的,并且无法保证可见性。
因为一旦字段被初始化就没有锁定,所以将字段声明为volatile
是至关重要的。这将确保可见性。
private volatile Helper helper = null;
【讨论】:
唯一可能成为错误的情况是线程在第一次读取时发现空引用,然后即使在进入同步块后也返回空。您能否举例说明此代码的可见性错误? @MarinVeršić 在字段初始化后发生的公共路径中存在可见性问题。您可能会看到一个明显的陈旧值。 我看到对变量的访问不受保护。但对我来说,这个例子中的一个错误并不是很明显。从我的角度来看,只有在第一次阅读之前发生第三次阅读,才会有可见性问题。这不是真的吗? @MarinVeršić 有趣的是,如果我们将synchronized
视为障碍,那么这些读取不应该能够跨越该障碍,我不认为这个答案包含所需的所有细节
感谢@Eugene,我想更详细地解释一下在这种情况下如何发生过时的读取【参考方案2】:
这里都解释了https://shipilev.net/blog/2014/safe-public-construction/#_singletons_and_singleton_factories,问题很简单。
... 请注意,我们在这段代码中对实例进行了多次读取,并且在 最少的“read 1”和“read 3”是没有任何 同步 ... 规范方面,如发生前所述 一致性规则,一个读操作可以观察到无序的写通过 比赛。这是为每个读取操作决定的,无论其他什么 动作已经读取了相同的位置。在我们的示例中, 意味着即使“读取 1”可以读取非空实例,代码 然后继续返回它,然后它又进行了一次活泼的阅读,它 可以读取一个空实例,它会被返回!
【讨论】:
这是我学习该模式的地方。感谢您发布链接。【参考方案3】:不,不存在传递关系。
JMM 背后的理念是定义 JVM 必须遵守的规则。如果 JVM 遵循这些规则,它们就可以根据需要重新排序和执行代码。
在您的示例中,第 2 次读取和第 3 次读取不相关 - 例如,使用 synchronized
或 volatile
不会引入内存屏障。因此,允许JVM按如下方式执行它:
public Helper getHelper()
final Helper toReturn = helper; // "3rd" read, reading null
if (helper == null) // First read of helper
synchronized (this)
if (helper == null) // Second read of helper
helper = new Helper(42);
return toReturn; // Returning null
然后您的调用将返回一个空值。然而,将创建一个单例值。但是,后续调用可能仍会获得空值。
正如建议的那样,使用 volatile 会引入新的内存屏障。另一种常见的解决方案是捕获读取的值并返回它。
public Helper getHelper()
Helper singleton = helper;
if (singleton == null)
synchronized (this)
singleton = helper;
if (singleton == null)
singleton = new Helper(42);
helper = singleton;
return singleton;
由于您依赖于局部变量,因此无需重新排序。一切都发生在同一个线程中。
【讨论】:
谢谢,我很惊讶在同步块之间的关系之前没有发生任何事情。它看起来如此微妙且容易出错。我必须更加小心,以免掉入这个坑中。 所以只是为了确定一下,如果要使助手成为易失性,则此代码将不再是错误的,因为易失性读/写引入了发生前发生前/后易失性访问的变量,而同步不会.我知道在 volatile 写入之前对所有变量的读/写不允许在 volatile 写入之后重新排序。易失性读取也是如此,在易失性读取之前不允许对所有变量读取/写入进行重新排序。从这个意义上说,volatile 比同步更严格,因为它创建了更强的屏障 我不确定我们真的可以这样比较synchronized
和volatile
。使用同步,块中发生的任何事情都将对同一元素上的任何下一个同步块可见 - 读取或写入。对于 volatile,它将取决于操作:读取 volatile 元素会设置读屏障,写入 volatile 元素会设置写屏障。在某种程度上,volatile
比synchronized
更灵活。但是synchronized
更容易同时管理多个元素 - 块中的所有操作 - 而volatile
即使不需要,也会要求您读取元素。
如果您不介意,我已经打开了一个单独的问题 (***.com/questions/52806674/…)。可能和这个问题没有直接关系
我只想让您注意这个答案(***.com/a/52806923/5182723),这表明在第一次和第三次读取之间的关系之前发生了,因为同步施加了障碍。你能对此发表评论吗?以上是关于Java中同步的可见性效果的主要内容,如果未能解决你的问题,请参考以下文章