为什么双重检查锁定中使用易失性

发布于 2024-12-11 19:40:22 字数 538 浏览 0 评论 0原文

Head First设计模式书中,具有双重检查锁定的单例模式已实现如下:

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

我不明白为什么使用易失性。难道易失性的使用会破坏使用双重检查锁定即性能的目的吗?

From Head First design patterns book, the singleton pattern with double checked locking has been implemented as below:

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

I don't understand why volatile is being used. Doesn't volatile usage defeat the purpose of using double checked locking i.e performance?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

心在旅行 2024-12-18 19:40:23

JCIP 书中有一个很好的资源可以帮助您理解为什么需要 volatile。维基百科也对该材料有体面的解释

真正的问题是,线程A可能在完成构造实例之前为实例分配内存空间。线程 B 将看到该分配并尝试使用它。这会导致线程 B 失败,因为它使用的是实例 的部分构造版本。

A good resource for understanding why volatile is needed comes from the JCIP book. Wikipedia has a decent explanation of that material as well.

The real problem is that Thread A may assign a memory space for instance before it is finished constructing instance. Thread B will see that assignment and try to use it. This results in Thread B failing because it is using a partially constructed version of instance.

空心↖ 2024-12-18 19:40:23

正如 @irreputable 所引用的, volatile 并不昂贵。即使价格昂贵,一致性也应该优先于性能。

对于懒惰单例来说,还有一种更干净优雅的方式。

public final class Singleton {
    private Singleton() {}
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
    private static class LazyHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
}

来源文章:Initialization-on-demand_holder_idiom 来自维基百科

在软件工程中,按需初始化持有者(设计模式)习惯用法是延迟加载的单例。在所有版本的 Java 中,该习惯用法都可以实现安全、高度并发的延迟初始化,并具有良好的性能

由于该类没有任何要初始化的静态变量,因此初始化可以轻松完成。

其中的静态类定义LazyHolder在JVM确定必须执行LazyHolder之前不会被初始化。

静态类LazyHolder仅在类Singleton上调用静态方法getInstance时执行,并且第一次发生这种情况时,JVM将加载并初始化LazyHolder类。

该解决方案是线程安全的,不需要特殊的语言结构(即易失性同步)。

As quoted by @irreputable, volatile is not expensive. Even if it is expensive, consistency should be given priority over performance.

There is one more clean elegant way for Lazy Singletons.

public final class Singleton {
    private Singleton() {}
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
    private static class LazyHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
}

Source article : Initialization-on-demand_holder_idiom from wikipedia

In software engineering, the Initialization on Demand Holder (design pattern) idiom is a lazy-loaded singleton. In all versions of Java, the idiom enables a safe, highly concurrent lazy initialization with good performance

Since the class does not have any static variables to initialize, the initialization completes trivially.

The static class definition LazyHolder within it is not initialized until the JVM determines that LazyHolder must be executed.

The static class LazyHolder is only executed when the static method getInstance is invoked on the class Singleton, and the first time this happens the JVM will load and initialize the LazyHolder class.

This solution is thread-safe without requiring special language constructs (i.e. volatile or synchronized).

颜漓半夏 2024-12-18 19:40:23

好吧,没有双重检查锁定来提高性能。这是一个破损的模式。

抛开情绪不谈,易失性在这里是因为如果没有它,当第二个线程传递instance == null时,第一个线程可能无法构造new Singleton() > 然而:除了实际创建对象的线程之外,没有人承诺对象的创建发生在分配给任何线程的实例之前。

易失性反过来在读取和写入之间建立发生之前关系,并修复损坏的模式。

如果您正在寻找性能,请改用holder内部静态类。

Well, there's no double-checked locking for performance. It is a broken pattern.

Leaving emotions aside, volatile is here because without it by the time second thread passes instance == null, first thread might not construct new Singleton() yet: no one promises that creation of the object happens-before assignment to instance for any thread but the one actually creating the object.

volatile in turn establishes happens-before relation between reads and writes, and fixes the broken pattern.

If you are looking for performance, use holder inner static class instead.

Hello爱情风 2024-12-18 19:40:23

之所以需要易失性是因为易失性

  • 在线程之间的Java变量可见性
  • 中有2种语义停止重新排序

所以在双重检查锁中没有易失性的问题是语句

< code>instance = new Singleton()

字节码主要有3个步骤,可以通过命令查看 javap -c Singleton.class

      17: new           #3                  // class Singleton
      20: dup
      21: invokespecial #4                  // Method "<init>":()V
  1. 为对象分配内存空间(尚未初始化)
  2. 创建一个变量指向该空间内存地址
  3. 调用构造函数来初始化对象

这 3 个步骤可以在运行时由 CPU 或 JVM 重新排序,在这种情况下,您可能会得到尚未完全初始化的实例。

通过使用易失性,JVM将插入monitorentermonitorexit以避免如下重新排序。

      10: monitorenter
      11: getstatic     #2                  // Field instance:LSingleton;
      14: ifnonnull     27
      17: new           #3                  // class Singleton
      20: dup
      21: invokespecial #4                  // Method "<init>":()V
      24: putstatic     #2                  // Field instance:LSingleton;
      27: aload_0
      28: monitorexit

所以单例需要volative

The reason why you need volatile is because volatile has 2 semantics in Java

  • variable visibility between threads
  • stop re-ordering

So the problem without volatile in the double checked lock is that statement

instance = new Singleton()

have 3 main steps in bytecode which can be viewed by command javap -c Singleton.class

      17: new           #3                  // class Singleton
      20: dup
      21: invokespecial #4                  // Method "<init>":()V
  1. Allocate memory space for the object (not initialized yet)
  2. Create a variable to point to this space memory address
  3. Call constructor to initialize the object

These 3 steps can be re-ordered during runtime by CPU or JVM which can be a case you will get an instance not fully initialized yet.

By having volatile JVM will insert monitorenter and monitorexit to avoid re-ordering as below.

      10: monitorenter
      11: getstatic     #2                  // Field instance:LSingleton;
      14: ifnonnull     27
      17: new           #3                  // class Singleton
      20: dup
      21: invokespecial #4                  // Method "<init>":()V
      24: putstatic     #2                  // Field instance:LSingleton;
      27: aload_0
      28: monitorexit

So volative is required for singleton.

眼眸里的快感 2024-12-18 19:40:23

如果您没有它,则在第一个线程将其设置为 null 后,第二个线程可能会进入同步块,并且您的本地缓存仍会认为它为 null。

第一个不是为了正确性(如果你是正确的那就会弄巧成拙),而是为了优化。

If you didn't have it, a second thread could get into the synchronized block after the first set it to null, and your local cache would still think it was null.

The first one is not for correctness (if it were you are correct that it would be self defeating) but rather for optimization.

如果没结果 2024-12-18 19:40:23

将变量声明为 易失性 可以保证对它的所有访问实际上都是从内存中读取其当前值。

如果没有易失性,编译器可能会优化对变量的内存访问(例如将其值保存在寄存器中),因此只有第一次使用变量时才会读取保存该变量的实际内存位置。如果变量在第一次和第二次访问之间被另一个线程修改,就会出现问题;第一个线程只有第一个(修改前)值的副本,因此第二个 if 语句测试变量值的过时副本。

Declaring the variable as volatile guarantees that all accesses to it actually read its current value from memory.

Without volatile, the compiler may optimize away the memory accesses to the variable (such as keeping its value in a register), so only the first use of the variable reads the actual memory location holding the variable. This is a problem if the variable is modified by another thread between the first and second access; the first thread has only a copy of the first (pre-modified) value, so the second if statement tests a stale copy of the variable's value.

空袭的梦i 2024-12-18 19:40:23

易失性读取本身并不昂贵。

您可以设计一个测试来在紧密循环中调用getInstance(),以观察易失性读取的影响;然而这个测试并不现实;在这种情况下,程序员通常会调用一次getInstance()并在使用期间缓存实例。

另一个实现是使用 final 字段(参见维基百科)。这需要额外的读取,这可能比易失性版本更昂贵。 最终版本在紧密循环中可能会更快,但正如之前所说,该测试没有实际意义。

A volatile read is not really expensive in itself.

You can design a test to call getInstance() in a tight loop, to observe the impact of a volatile read; however that test is not realistic; in such situation, programmer usually would call getInstance() once and cache the instance for the duration of use.

Another impl is by using a final field (see wikipedia). This requires an additional read, which may become more expensive than the volatile version. The final version may be faster in a tight loop, however that test is moot as previously argued.

昔梦 2024-12-18 19:40:23

双重检查锁定是一种防止在多线程环境中调用 getInstance 方法时创建另一个单例实例的技术。

注意

  • Singleton 实例在初始化之前会被检查两次。
  • 仅在首次检查单例实例后才使用同步临界区,以提高性能。
  • 实例成员声明中的易失性关键字。这将告诉编译器始终从主内存而不是 CPU 缓存读取和写入。使用 volatile 变量保证发生之前关系,所有写入都将在实例变量的任何读取之前发生。

缺点

  • 由于它需要 volatile 关键字才能正常工作,因此它与 Java 1.4 及更低版本不兼容。问题在于,无序写入可能允许在执行单例构造函数之前返回实例引用。
  • 由于易失性变量的缓存下降而导致性能问题。
  • Singleton 实例在初始化之前会被检查两次。
  • 它非常冗长,使得代码难以阅读。

单例模式有多种实现,每一种都有优点和缺点。

  • 热切加载单例
  • 双重检查锁定单例
  • 按需初始化持有者习语
  • 基于枚举的单例

详细描述 他们每个人都太冗长了,所以我只是放了一个好文章的链接 - 有关 Singleton 的所有信息

Double checked locking is a technique to prevent creating another instance of singleton when call to getInstance method is made in multithreading environment.

Pay attention

  • Singleton instance is checked twice before initialization.
  • Synchronized critical section is used only after first checking singleton instance for that reason to improve performance.
  • volatile keyword on the declaration of the instance member. This will tell the compiler to always read from, and write to, main memory and not the CPU cache. With volatile variable guaranteeing happens-before relationship, all the write will happen before any read of instance variable.

Disadvantages

  • Since it requires the volatile keyword to work properly, it's not compatible with Java 1.4 and lower versions. The problem is that an out-of-order write may allow the instance reference to be returned before the singleton constructor is executed.
  • Performance issue because of decline cache for volatile variable.
  • Singleton instance is checked two times before initialization.
  • It's quite verbose and it makes the code difficult to read.

There are several realization of singleton pattern each one with advantages and disadvantages.

  • Eager loading singleton
  • Double-checked locking singleton
  • Initialization-on-demand holder idiom
  • The enum based singleton

Detailed description each of them is too verbose so I just put a link to a good article - All you want to know about Singleton

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文