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 次读取不相关 - 例如,使用 synchronizedvolatile 不会引入内存屏障。因此,允许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 比同步更严格,因为它创建了更强的屏障 我不确定我们真的可以这样比较synchronizedvolatile。使用同步,块中发生的任何事情都将对同一元素上的任何下一个同步块可见 - 读取或写入。对于 volatile,它将取决于操作:读取 volatile 元素会设置读屏障,写入 volatile 元素会设置写屏障。在某种程度上,volatilesynchronized 更灵活。但是synchronized 更容易同时管理多个元素 - 块中的所有操作 - 而volatile 即使不需要,也会要求您读取元素。 如果您不介意,我已经打开了一个单独的问题 (***.com/questions/52806674/…)。可能和这个问题没有直接关系 我只想让您注意这个答案(***.com/a/52806923/5182723),这表明在第一次和第三次读取之间的关系之前发生了,因为同步施加了障碍。你能对此发表评论吗?

以上是关于Java中同步的可见性效果的主要内容,如果未能解决你的问题,请参考以下文章

java多线程2.线程安全之可见性

JAVA的原子性和可见性,线程同步的理解

[Java并发编程实战] 共享对象之可见性

[Java并发编程实战] 共享对象之可见性

java中volatilesynchronized

java并发编程(十四)同步问题的内存可见性