【问题标题】:Java lazy thread safe singleton with implemented with final field使用 final 字段实现的 Java 惰性线程安全单例
【发布时间】:2018-11-30 18:56:33
【问题描述】:

我不明白为什么这里需要局部变量:

public class FinalWrapper<T> {
    public final T value;
    public FinalWrapper(T value) {
        this.value = value;
    }
}

public class Foo {
   private FinalWrapper<Helper> helperWrapper;

   public Helper getHelper() {
      FinalWrapper<Helper> tempWrapper = helperWrapper;

      if (tempWrapper == null) {
          synchronized(this) {
              if (helperWrapper == null) {
                  helperWrapper = new FinalWrapper<Helper>(new Helper());
              }
              tempWrapper = helperWrapper;
          }
      }
      return tempWrapper.value;
   }
}

此代码来自:https://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java。如果我们没有这个局部变量,我们会遇到什么问题?根据维基文章:

Java 5 中 final 字段的语义可用于安全地发布帮助对象,而无需使用 volatile。局部变量 tempWrapper 是正确性所必需的:简单地使用 helperWrapper 进行 null 检查和 return 语句可能会由于 Java 内存模型下允许的读取重新排序而失败。此实现的性能不一定比 volatile 实现更好。

提前致谢。

【问题讨论】:

  • 你最好使用嵌套私有类的惰性持有者习惯用法,它通过类加载机制强制执行单例行为。
  • 我在下面更新了我的答案,以提供有关重新排序的更多信息。我希望它会耗尽主题。

标签: java concurrency singleton final


【解决方案1】:

要了解根本问题,让我们从代码中删除局部变量:

public class Foo {
    private FinalWrapper<Helper> helperWrapper;

    public Helper getHelper() {
        if (helperWrapper == null) {
            synchronized(this) {
                if (helperWrapper == null) {
                    helperWrapper = new FinalWrapper<Helper>(new Helper());
                }
            }
        }
        return helperWrapper.value;
    }
}

在这种情况下我们有三个读取:

  1. 外部空检查。
  2. 内部空检查。
  3. 返回前的读取。

问题在于,由于读取重新排序,第一次读取可以返回 non-null 值,第三次读取可以返回 null。这意味着第三次读取发生在第一次之前,这应该确保 helperWrapper 被初始化...

添加局部变量解决了这个问题,因为我们将helperWrapper 值分配给tempWrapper,然后读取tempWrapper 的顺序无关紧要。如果它有一个非空值,则它同时用于空值检查和返回语句。

之所以会发生这种情况,是因为 Java 内存模型仅出于优化目的而允许对操作进行此类重新排序。看here的引用:

什么是重新排序?

访问程序变量的情况有很多 (对象实例字段、类静态字段和数组元素)可能 似乎以不同于指定的顺序执行 程序。编译器可以随意随意排序 以优化为名的指令。处理器可以执行 在某些情况下指令不正常。数据可能是 在寄存器、处理器高速缓存和主存储器之间移动 与程序指定的顺序不同。

[...]

编译器、运行时和硬件应该共同创建 好像串行语义的错觉,这意味着在一个 单线程程序,程序应该是观察不到的 重新排序的影响。但是,重新排序可能会在 不正确同步的多线程程序,其中一个线程 能够观察其他线程的影响,并且可能能够 检测变量访问对 a 中的其他线程可见 与程序中执行或指定的顺序不同。

[...]

【讨论】:

  • 感谢您的回答,@cbartosiak!不幸的是,我不明白这种特定的重新排序是如何可能的。让我们假设我们分析没有局部变量的代码(您已经编写过)。如果我们假设这样的重新排序是可能的(第三次读取在第一次之前完成)并且如果我们在单线程程序中运行它,它将抛出 NullPointerException(因为变量在第二次读取之后被初始化)。那么这怎么可能呢?我相信你,因为我在这里看到了一个类似的例子:jeremymanson.blogspot.bg/2008/12/benign-data-races-in-java.html(最后一个例子)
  • 但是,当这种重新排序可能会改变单线程执行时,这怎么可能呢?提前致谢!
  • 在单线程执行中,顺序无关紧要,因为 helperWrapper 的值不能在读取之间更改。该问题出现在多线程执行中,其中另一个线程可以在读取之间写入 helperWrapper。有帮助吗?
  • 更准确地说——在单线程执行中,如果第三次读取返回 null,则第一次也返回 null。无论顺序如何,返回的值都是一样的。但是,在多线程执行中,如果第三次读取返回 null,则第一次读取可以返回非 null 或 null,具体取决于另一个线程是否在这两次读取之间写入。这称为数据竞争。由于重新排序,这是可能的。
  • 最后一件事 - NullPointerException 只有当我们在第一次读取时获得非空值,但在第三次读取时才抛出空值。您不会在单线程执行中看到这种情况。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-12-18
  • 1970-01-01
相关资源
最近更新 更多