咱们第一次写的单例模式是下面这样的:安全
1 public class Singleton { 2 private static Singleton instance = null; 3 public static Singleton getInstance() { 4 if(null == instance) { // line A 5 instance = new Singleton(); // line B 6 } 7 8 return instance; 9 10 } 11 }
假设这样的场景:两个线程并发调用Singleton.getInstance(),假设线程一先判断instance是否为null,即代码中line A进入到line B的位置。刚刚判断完毕后,JVM将CPU资源切换给线程二,因为线程一还没执行line B,因此instance仍然为空,所以线程二执行了new Singleton()操做。片刻以后,线程一被从新唤醒,它执行的仍然是new Singleton()操做,这样问题就来了,new出了两个instance,这还能叫单例吗?并发
紧接着,咱们再作单例模式的第二次尝试:函数
1 public class Singleton { 2 private static Singleton instance = null; 3 public synchronized static Singleton getInstance() { 4 if(null == instance) { 5 instance = new Singleton(); 6 } 7 8 return instance; 9 10 } 11 }
比起第一段代码仅仅在方法中多了一个synchronized修饰符,如今能够保证不会出线程问题了。可是这里有个很大(至少耗时比例上很大)的性能问题。除了第一次调用时是执行了Singleton的构造函数以外,之后的每一次调用都是直接返回instance对象。返回对象这个操做耗时是很小的,绝大部分的耗时都用在synchronized修饰符的同步准备上,所以从性能上来讲很不划算。性能
继续把代码改为下面这样:优化
1 public class Singleton { 2 private static Singleton instance = null; 3 public static Singleton getInstance() { 4 synchronized (Singleton.class) { 5 if(null == instance) { 6 instance = new Singleton(); 7 } 8 } 9 10 return instance; 11 12 } 13 }
基本上,把synchronized移动到代码内部是没有什么意义的,每次调用getInstance()仍是要进行同步。同步自己没有问题,可是咱们只但愿在第一次建立instance实例的时候进行同步,所以有了下面的写法——双重锁定检查(DCL,Double Check Lock)。spa
1 public class Singleton { 2 private static Singleton instance = null; 3 public static Singleton getInstance() { 4 if(null == instance) { // 线程二检测到instance不为空 5 synchronized (Singleton.class) { 6 if(null == instance) { 7 instance = new Singleton(); // 线程一被指令重排,先执行了赋值,但还没执行完构造函数(即未完成初始化) 8 } 9 } 10 } 11 12 return instance; // 后面线程二执行时将引起:对象还没有初始化错误 13 14 } 15 }
看样子已经达到了要求,除了第一次建立对象以外,其它的访问在第一个if中就返回了,所以不会走到同步块中,已经完美了吗?线程
如上代码段中的注释:假设线程一执行到instance = new Singleton()这句,这里看起来是一句话,但实际上其被编译后在JVM执行的对应会变代码就发现,这句话被编译成8条汇编指令,大体作了三件事情:code
1)给instance实例分配内存;对象
2)初始化instance的构造器;blog
3)将instance对象指向分配的内存空间(注意到这步时instance就非null了)
若是指令按照顺序执行倒也无妨,但JVM为了优化指令,提升程序运行效率,容许指令重排序。如此,在程序真正运行时以上指令执行顺序多是这样的:
a)给instance实例分配内存;
b)将instance对象指向分配的内存空间;
c)初始化instance的构造器;
这时候,当线程一执行b)完毕,在执行c)以前,被切换到线程二上,这时候instance判断为非空,此时线程二直接来到return instance语句,拿走instance而后使用,接着就瓜熟蒂落地报错(对象还没有初始化)。
具体来讲就是synchronized虽然保证了线程的原子性(即synchronized块中的语句要么所有执行,要么一条也不执行),但单条语句编译后造成的指令并非一个原子操做(便可能该条语句的部分指令未获得执行,就被切换到另外一个线程了)。
根据以上分析可知,解决这个问题的方法是:禁止指令重排序优化,即便用volatile变量。
1 public class Singleton { 2 private volatile static Singleton instance = null; 3 public static Singleton getInstance() { 4 if(null == instance) { 5 synchronized (Singleton.class) { 6 if(null == instance) { 7 instance = new Singleton(); 8 } 9 } 10 } 11 12 return instance; 13 14 } 15 }
将变量instance使用volatile修饰便可实现单例模式的线程安全。
关于volatile的用法在此不展开,以后会另行介绍。