【转】深刻浅出单实例SINGLETON设计模式

单例模式理解起来应该不难,可是若是是在多线程下应该如何安全地实现单例模式呢?,看到一篇挺好的文章,顺手转过来,待往后细细回味。html

原文出处:深刻浅出单实例SINGLETON设计模式shell

 


 

Singleton的教学版本

这里,我将直接给出一个Singleton的简单实现,由于我相信你已经有这方面的一些基础了。咱们姑且把这个版本叫作1.0版设计模式

 

 1 // version 1.0
 2 public class Singleton {  3     private static Singleton singleton = null;  4     private Singleton() { }  5     public static Singleton getInstance() {  6         if (singleton== null) {  7             singleton= new Singleton();  8  }  9         return singleton; 10  } 11 }

 

在上面的实例中,我想说明下面几个Singleton的特色:(下面这些东西多是尽人皆知的,没有什么新鲜的)安全

  1. 私有(private)的构造函数,代表这个类是不可能造成实例了。这主要是怕这个类会有多个实例。
  2. 即然这个类是不可能造成实例,那么,咱们须要一个静态的方式让其造成实例:getInstance()。注意这个方法是在new本身,由于其能够访问私有的构造函数,因此他是能够保证明例被建立出来的。
  3. 在getInstance()中,先作判断是否已造成实例,若是已造成则直接返回,不然建立实例。
  4. 所造成的实例保存在本身类中的私有成员中。
  5. 咱们取实例时,只须要使用Singleton.getInstance()就好了。

固然,若是你以为知道了上面这些事情后就学成了,那得给你当头棒喝一下了,事情远远没有那么简单。多线程

 

Singleton的实际版本

上面的这个程序存在比较严重的问题,由于是全局性的实例,因此,在多线程状况下,全部的全局共享的东西都会变得很是的危险,这个也同样,在多线程状况下,若是多个线程同时调用getInstance()的话,那么,可能会有多个进程同时经过 (singleton== null)的条件检查,因而,多个实例就建立出来,而且极可能形成内存泄露问题。嗯,熟悉多线程的你必定会说——“咱们须要线程互斥或同步”,没错,咱们须要这个事情,因而咱们的Singleton升级成1.1版,以下所示:

 1 // version 1.1
 2 public class Singleton  3 {  4     private static Singleton singleton = null;  5     private Singleton() { }  6     public static Singleton getInstance() {  7         if (singleton== null) {  8     synchronized (Singleton.class) {  9                 singleton= new Singleton(); 10  } 11  } 12         return singleton; 13  } 14 }

 

嗯,使用了Java的synchronized方法,看起来不错哦。应该没有问题了吧?!错!这仍是有问题!为何呢?前面已经说过,若是有多个线程同时经过(singleton== null)的条件检查(由于他们并行运行),虽然咱们的synchronized方法会帮助咱们同步全部的线程,让咱们并行线程变成串行的一个一个去new,那不仍是同样的吗?一样会出现不少实例。嗯,确实如此!看来,还得把那个判断(singleton== null)条件也同步起来。因而,咱们的Singleton再次升级成1.2版本,以下所示:函数

 1 // version 1.2
 2 public class Singleton  3 {  4     private static Singleton singleton = null;  5     private Singleton() { }  6     public static Singleton getInstance() {  7 synchronized (Singleton.class) {  8             if (singleton== null) {  9             singleton= new Singleton(); 10  } 11  } 12         return singleton; 13  } 14 }

 

不错不错,看似很不错了。在多线程下应该没有什么问题了,不是吗?的确是这样的,1.2版的Singleton在多线程下的确没有问题了,由于咱们同步了全部的线程。只不过嘛……,什么?!还不行?!是的,仍是有点小问题,咱们原本只是想让new这个操做并行就能够了,如今,只要是进入getInstance()的线程都得同步啊,注意,建立对象的动做只有一次,后面的动做全是读取那个成员变量,这些读取的动做不须要线程同步啊。这样的做法感受很是极端啊,为了一个初始化的建立动做,竟然让咱们达上了全部的读操做,严重影响后续的性能啊!性能

还得改!嗯,看来,在线程同步前还得加一个(singleton== null)的条件判断,若是对象已经建立了,那么就不须要线程的同步了。OK,下面是1.3版的Singleton。优化

// version 1.3
public class Singleton { private static Singleton singleton = null; private Singleton() { } public static Singleton getInstance() { if (singleton== null) { synchronized (Singleton.class) { if (singleton== null) { singleton= new Singleton(); } } } return singleton; } }

 

感受代码开始变得有点罗嗦和复杂了,不过,这多是最不错的一个版本了,这个版本又叫“双重检查”Double-Check。下面是说明:spa

  1. 第一个条件是说,若是实例建立了,那就不须要同步了,直接返回就行了。
  2. 否则,咱们就开始同步线程。
  3. 第二个条件是说,若是被同步的线程中,有一个线程建立了对象,那么别的线程就不用再建立了。

至关不错啊,干得很是漂亮!请你们为咱们的1.3版起立鼓掌!线程

可是,若是你认为这个版本大攻告成,你就错了。

主要在于singleton = new Singleton()这句,这并不是是一个原子操做,事实上在 JVM 中这句话大概作了下面 3 件事情。

  1. 给 singleton 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量,造成实例
  3. 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)

可是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序多是 1-2-3 也多是 1-3-2。若是是后者,则在 3 执行完毕、2 未执行以前,被线程二抢占了,这时 instance 已是非 null 了(但却没有初始化),因此线程二会直接返回 instance,而后使用,而后瓜熟蒂落地报错。

对此,咱们只须要把singleton声明成 volatile 就能够了。下面是1.4版:

 

// version 1.4
public class Singleton { private volatile static Singleton singleton = null; private Singleton() { } public static Singleton getInstance() { if (singleton== null) { synchronized (Singleton.class) { if (singleton== null) { singleton= new Singleton(); } } } return singleton; } }

使用 volatile 有两个功用:

1)这个变量不会在多个线程中存在复本,直接从内存读取。

2)这个关键字会禁止指令重排序优化。也就是说,在 volatile 变量的赋值操做后面会有一个内存屏障(生成的汇编代码上),读操做不会被重排序到内存屏障以前。

可是,这个事情仅在Java 1.5版后有用,1.5版以前用这个变量也有问题,由于老版本的Java的内存模型是有缺陷的。

 

原文出处:https://coolshell.cn/articles/265.html

相关文章
相关标签/搜索