首页 > 解决方案 > 如何避免在单例模式中使用 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 关键字的性能开销。我们如何避免它?

标签: javadesign-patternsstaticsingletonvolatile

解决方案


你有一个严重的误解volatile,但公平地说,互联网和 stackoverflow 包括只是被错误或不完整的答案污染了。我也承认我认为我对此有很好的把握,但有时不得不重新阅读一些东西。

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

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

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

  private Singleton instance;

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

但是现在每个需要它instance的线程都必须竞争锁并且必须进入那个同步块。

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

  private Singleton instance; // no volatile

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

就这么简单——那就是坏了。这并不容易解释。

  • 它被破坏了,因为有两个独立的读取; instanceJMM 允许对这些进行重新排序;因此看不到空值是完全有效的;if (instance == null)whilereturn instance;看到并返回 a null。是的,这是违反直觉的,但完全有效且可证明(我可以jcstress在 15 分钟内编写一个测试来证明这一点)。

  • 第二点有点棘手。假设您的单身人士有一个需要设置的字段。

看这个例子:

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不是空值,但在执行时instance.getSome()会看到空值。正确的方法是(加上创建实例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只要您的代码是正确的,您就不应该关心读取的内容;所以不要担心“从主内存读取”(或者最好不要在没有部分理解的情况下使用这个短语)。


推荐阅读