Detailed explanation of singleton mode

How to correctly write the singleton pattern

1. Lazy, thread-unsafe 

This code is simple and clear, and uses lazy loading mode, but there are fatal problems. When multiple threads call getInstance() in parallel, multiple instances are created. That is to say, it does not work properly under multi-threading.

public class Singleton {

   private static Singleton instance;

   private Singleton (){}

   public static Singleton getInstance() {

          if (instance == null) {

                 instance = new Singleton();

          }

          return instance;

   }

}

 

2. Lazy, thread-safe

In order to solve the above problem, the easiest way is to make the whole getInstance() method synchronized (synchronized)

public static synchronized Singleton getInstance() {

      if (instance == null) {

              instance = new Singleton();

       }

       return instance;

}

 Although it is thread-safe and solves the problem of multiple instances, it is not efficient. Because only one thread can call the getInstance() method at any time. But the synchronous operation is only needed when it is called for the first time, that is, when the singleton instance object is first created. This leads to the double-checked lock

 

 

3. Double check lock 

The double checked locking pattern is a method of locking using synchronized blocks. Programmers call this a double-checked lock because instance == null is checked twice, once outside the synchronized block and once inside the synchronized block. Why is it checked again within the synchronized block? Because there may be multiple threads entering the if outside the synchronized block together, multiple instances will be generated if the second check is not performed in the synchronized block.

public static Singleton getSingleton() {

     if (instance == null) { //Single Checked              

          synchronized (Singleton.class) {

                if (instance == null) { //Double Checked    

                      instance = new Singleton();

                 }

           }

      }

       return instance ;

}

This code looks perfect, unfortunately it is buggy. The main reason is the sentence instance = new Singleton(), which is not an atomic operation. In fact, in the JVM, this sentence probably does the following three things.

1. Allocate memory to instance

2. Call the constructor of Singleton to initialize member variables

3. Point the instance object to the allocated memory space (the instance will be non-null after this step)

But there are optimizations for instruction reordering in the JVM's just-in-time compiler. That is to say, the order of the second and third steps above is not guaranteed, and the final execution order may be 1-2-3 or 1-3-2. If it is the latter, it is preempted by thread 2 before 3 is executed and 2 is not executed. At this time, instance is already non-null (but not initialized), so thread 2 will directly return to instance, then use it, and then logically report an error.

我们只需要将 instance 变量声明成 volatile 就可以了

public class Singleton {

  private volatile static Singleton instance; //声明成 volatile   private Singleton (){}

public static Singleton getSingleton() {

          if (instance == null) {

                  synchronized (Singleton.class) {

                                if (instance == null) {

                                      instance = new Singleton();

                                 }

                   }

          }

         return instance;

 }

些人认为使用 volatile 的原因是可见性,也就是可以保证线程在本地不会存有 instance 的副本,每次都是去主内存中读取。但其实是不对的。使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。

但是特别注意在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。

相信你不会喜欢这种复杂又隐含问题的方式,当然我们有更好的实现线程安全的单例模式的办法

 

 

 

4.饿汉式 static final field
这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的

public class Singleton{

       //类加载时就初始化

       private static final Singleton instance = new Singleton();

       private Singleton(){}

       public static Singleton getInstance(){

                return instance;

       }

}

这种写法如果完美的话,就没必要在啰嗦那么多双检锁的问题了。缺点是它不是一种懒加载模式(lazy initialization),单例会在加载类后一开始就被初始化,即使客户端没有调用 getInstance()方法。饿汉式的创建方式在一些场景中将无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了

 

5.静态内部类 static nested class

我比较倾向于使用静态内部类的方法,这种方法也是《Effective Java》上所推荐的

 

 

public class Singleton {

       private static class SingletonHolder {

                private static final Singleton INSTANCE = new Singleton();

       }

        private Singleton (){}             

       public static final Singleton getInstance() {

                return SingletonHolder.INSTANCE;

       }

}


这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本

6.枚举 Enum

用枚举写单例实在太简单了!这也是它最大的优点。下面这段代码就是声明枚举实例的通常做法

public enum EasySingleton{

      INSTANCE;

}

我们可以通过EasySingleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。但是还是很少看到有人这样写,可能是因为不太熟悉吧
总结

 一般来说,单例模式有五种写法:懒汉、饿汉、双重检验锁、静态内部类、枚举。上述所说都是线程安全的实现,文章开头给出的第一种方法不算正确的写法。 就我个人而言,一般情况下直接使用饿汉式就好了,如果明确要求要懒加载(lazy initialization)会倾向于使用静态内部类,如果涉及到反序列化创建对象时会试着使用枚举的方式来实现单例

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325928361&siteId=291194637