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

根据 以头为先设计模式手册,双重检查锁定的单例模式已经实现如下:

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;
}
}

我不明白为什么使用 volatilevolatile的使用是否违背了使用双重检查锁定(即性能)的目的?

29896 次浏览

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.

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.

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.

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.

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.

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).

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

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.