【问题标题】:How to avoid the performance overhead of using volatile in singleton pattern?如何避免在单例模式中使用 volatile 的性能开销?
【发布时间】:2019-07-10 06:49:46
【问题描述】:

单例模式的代码:

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 关键字的性能开销。我们如何避免它?

【问题讨论】:

  • 如果不需要,请避免仔细检查。!
  • 首先,您混淆了volatilesynchronized 块。两者都提供原子性(意味着一个线程所做的更改对其他人可见)但synchronized 块还提供互斥,这意味着它不允许超过一个线程一次访问它。我的观点是volatile 用于当你想要原子性但又不想互斥时。您可能不应该将两者结合使用,因为它违背了使用 volatile 关键字的目的。
  • 我想他是想避免多线程发现obj为null的情况下的双重初始化。
  • 要添加到@MushifAliNawaz,您可能想要的是volatile 而不是synchronized。从技术上讲,通过检查实现此getInstance() 的唯一安全方法是使用synchronized 块 - 如果您希望确保只调用一次构造函数。这就是你想在构建重量级时使用的模式,或者由于其他原因应该只发生一次。这也是正确的。但是,通常,出于其他原因,您想要一个该类型的对象 - 只要只使用一个,构造两个就可以了。然后跳过synchronized 并使用volatile
  • @MushifAliNawaz “两者都提供原子性” - 不,应该是“两者都提供可见性”,易失性变量不是原子的。还有“你可能不应该同时使用两者,因为它违背了使用 volatile 关键字的目的。” - 这就像说不要使用 DCL,这至少是不准确的。

标签: java design-patterns static singleton volatile


【解决方案1】:

您严重误解了 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; 
  }

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

  • 因为instance 有两个独立读取而被破坏; JMM 允许对这些进行重新排序;因此,if (instance == 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; 
}

由于写入volatileinstance = 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 读取的内容;所以不要担心“从主内存读取”(或者最好不要在没有部分理解的情况下使用这个短语)。

【讨论】:

  • 您确实误解了volatile 的含义,尽管您的解释仍然足够好。 volatile 所做的正是,这反映在“可见性”解释中的是,它总是会从主内存中获取变量值,即使它已经在 CPU 缓存中。无论如何,存储都会刷新到主内存; volatile 对此影响不大。这确实会影响编译器在重新排序方面可以做什么。但是,volatile确实在多线程应用程序中读取时会产生显着的性能开销。
  • 严格来说,您不需要volatilesynchronized,如果您能以某种方式确保创建单例实例的第一次写入发生在任何其他线程尝试读取实例变量之前。任何其他线程都会尝试从缓存中获取值,失败,然后从主内存中检索它。当您无法确保这一点时,或者您正在处理一个被大量修改的变量时,即不是单例时,这是非常危险的。
  • 不过,不要太担心对象成员(getSome()/setSome() 和底层的some)。实例变量仅保存对内存中 Singleton 对象的引用。也就是说,只要 Singleton 保持相同的对象,您将始终引用相同的内存,这使得使用 CPU 缓存是安全的。但是,如果setSome() 被调用很多次,我肯定会使用some 成员volatile,原因如上所述。
  • TL;DR 是,如果有疑问,请始终使用volatile 来确保线程间的值始终相同。有明显的性能影响,但这只有在您访问值很多时才重要,并且您每次都关心它是否是最新的。
  • synchronized 另一方面确保new Singleton() 行只执行一次。当创建单例的成本很高时,或者当您无法确保在创建实例后发生读取线程时,这一点至关重要(参见第一条评论)。如果有疑问,请使用它。如果您同时使用两者,DCL 将确保不会不必要地输入 synchronized 块,这有助于提高性能。
【解决方案2】:

如果你想避免使用volatile,那么你可以在类加载时初始化,使用私有构造函数来避免创建新的实例。

public class Singleton{
    //Initialized when class loading
    private static final Singleton INSTANCE = new Singleton();

    //To avoid creating new instance of Singleton
    private Singleton(){}

    public static Singleton getSingleton(){
        return INSTANCE;
    }
}

【讨论】:

  • 如果这是一个分布式应用程序(在多个处理器上运行)会发生什么?它将创建多个单独的实例。这就是为什么会有volatile这个概念的原因。
  • @MushifAliNawaz 你是什么意思多个单独的实例?这是一个单例 - 每个类加载器一个。 “分布式应用程序(在多个处理器上运行)”是什么意思?
  • @MushifAliNawaz,Singleton 应该有一个实例。但这并不能确保每个 JVM 只有一个实例,它只能确保每个类加载器有一个实例。当使用多个类加载器时,每个类加载器都有自己的。
【解决方案3】:

您可以使用Lazy initialization with Holder static class

class Singleton 
{ 

    private Singleton() {} 

    private static class LazyLoader{ 
        static final Singleton obj = new Singleton();
    }

    public static Singleton getInstance() 
    { 
        return LazyLoader.obj;
    } 
} 

这里要注意的重要一点是构造函数应该是故障安全的,否则类加载器会抛出NoClassDefFoundError

【讨论】:

    【解决方案4】:

    你应该使用枚举来实现单例。

    Joshua Bloch 建议使用 Enum 来实现 Singleton 设计模式,因为 Java 将确保任何枚举值在 Java 中只实例化一次 程序。缺点是枚举类型有些不灵活;为了 例如,它不允许延迟初始化。

    public enum EnumSingleton {
        INSTANCE;
        int value;
        public int getValue() {
            return value;
        }
        public void setValue(int value) {
            this.value = value;
        }
    }
    
    public class EnumDemo {
        public static void main(String[] args) {
            EnumSingleton singleton = EnumSingleton.INSTANCE;
            System.out.println(singleton.getValue());
            singleton.setValue(2);
            System.out.println(singleton.getValue());
        }
    }
    

    这篇文章很好地列出了使用枚举的其他好处: java singleton instantiation

    【讨论】:

      猜你喜欢
      • 2023-03-12
      • 2014-05-21
      • 1970-01-01
      • 2012-04-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多