java - 如何避免在单例模式中使用 volatile 的性能开销?

标签 java design-patterns static singleton volatile

说出单例模式的代码:

class Singleton 
{ 
    private volatile static Singleton obj; 

    private Singleton() {} 

    public static Singleton getInstance() 
    { 
        if (obj == null) 
        { 
            synchronized (Singleton.class) 
            { 
                if (obj==null) 
                    obj = new Singleton(); 
            } 
        } 
        return obj; 
    } 
} 

上面代码中的obj被标记为Volatile,这意味着每当代码中使用obj时,它总是从主内存中获取而不是使用缓存值。因此,每当需要执行 if(obj==null) 时,它都会从主内存中获取 obj,尽管它的值是在上一次运行时设置的。这是使用 volatile 关键字的性能开销。我们如何避免它?

最佳答案

您严重误解了 volatile 的作用,但公平地说,互联网和 stackoverflow 包括关于此的错误或不完整的答案。我也承认我认为我对它有很好的把握,但有时不得不重新阅读一些东西。

您在那里展示的内容被称为“双重检查锁定”成语,它是创建单例的完全有效的用例。问题是你是否真的需要它(另一个答案显示了一种更简单的方法,或者如果你愿意,你也可以阅读“枚举单例模式”)。有点滑稽的是,有多少人知道这个习语需要 volatile,但不能真正告诉为什么需要它。

DCL 主要做了两件事——确保原子性(多个线程不能同时进入同步块(synchronized block))并确保一旦创建,所有线程都会看到创建的实例,称为可见度。同时,它确保同步块(synchronized block)将被单次进入,之后的所有线程都不需要这样做。

您可以通过以下方式轻松完成:

  private Singleton instance;

  public Singleton get() {
    synchronized (this) {
      if (instance == null) {
        instance = new Singleton();
      }
      return instance;
    }
  }

但是现在每个需要那个实例的线程都必须竞争锁并且必须进入那个同步块(synchronized block)。

有些人认为:“嘿,我可以解决这个问题!”并写入(因此进入同步块(synchronized block)一次):

  private Singleton instance; // no volatile

  public Singleton get() {
    if (instance == null) {  
      synchronized (this) {
        if (instance == null) { 
          instance = new Singleton();
        }
      }
    }
    return instance; 
  }

就这么简单 - 这就是损坏。而这并不容易解释。

  • 它坏了,因为实例有两次独立读取; JMM 允许重新排序;因此 完全有效 if (instance == null) 没有看到 null;而 return instance; 看到并返回一个 null。是的,这是违反直觉的,但完全有效且可证明(我可以在 15 分钟内编写一个 jcstress 测试来证明这一点)。

  • 第二点有点棘手。假设你的单例有一个你需要设置的字段。

看这个例子:

static class Singleton {

    private Object some;

    public Object getSome() {
        return some;
    }

    public void setSome(Object some) {
        this.some = some;
    }
}

然后您编写这样的代码来提供该单例:

private Singleton instance;

public Singleton get() {
    if (instance == null) {  
        synchronized (this) {
            if (instance == null) { 
                instance = new Singleton();
                instance.setSome(new Object());
            }
        }
    }
    return instance; 
}

由于写入volatile(instance = new Singleton();)发生在设置你需要的字段 instance.setSome(new Object());;某些读取此实例的线程可能会看到 instance 不为 null,但是在执行 instance.getSome() 时会看到 null。正确的做法是(加上使实例 volatile):

 public Singleton get() {
    if (instance == null) {
        synchronized (this) {
            if (instance == null) {
                Singleton copy = new Singleton();
                copy.setSome(new Object());
                instance = copy;
            }
        }
    }
    return instance; 
}

因此 volatile安全发布所必需的;这样所有线程都可以“安全地”看到已发布的引用 - 它的所有字段都已初始化。还有一些其他方法可以安全地发布引用,例如在构造函数中设置 final 等。

事实:读比写便宜;只要您的代码是正确的,您就不必关心 volatile 读取的内容;所以不要担心“从主内存读取”(或者最好不要在没有部分理解的情况下使用这个短语)。

关于java - 如何避免在单例模式中使用 volatile 的性能开销?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56964868/

相关文章:

java - Rest 和 Struts 1.x 结合在一起

java - 调整 Builder 模式以进行方法调用

java - 类似的输入对话框创建模式/Swing

c# - 静态、常量和只读字段的内存分配在哪里?

java - 使用 Runtime 从 Java 运行 Maven 命令

java - 查找连续的数字?

java - 如何将资源目录中的歌曲添加到播放列表

c# - 如何在一笔交易中移动记录?或者 SQL 中的生产者-消费者模式

c++ - 访问类内的二维数组时出错

java - 为什么该类的每个对象对其成员具有相同的值?