问题分析
上篇文章,我们使用了synchronized关键字解决了多线程环境下的单例模式的线程安全问题。
/**
* @Auther: ming.wang
* @Date: 2019/1/6 19:25
* @Description:
*/
public class LazySingleton {
private static LazySingleton lazySingleton=null;
private LazySingleton() {
}
public synchronized static LazySingleton getInstance(){
if (null==lazySingleton)
{
lazySingleton=new LazySingleton();
}
return lazySingleton;
}
}
但同时也有个小小的缺憾,就是synchronized关键字开销比较大。本篇文章就来分析改如何优化上述代码。
优化
其实优化思路已经在标题中体现了,就是使用双重检查的方式来减少进锁的几率。闲言碎语不要讲,直接上代码
/**
* @Author: ming.wang
* @Date: 2019/2/20 14:45
* @Description:
*/
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton instance=null;
private LazyDoubleCheckSingleton() {
}
public static LazyDoubleCheckSingleton getInstance(){
if (null==instance) {//第一个 if(instance==null),其实是为了解决代码中的效率问题,只有instance为null的时候,才进入synchronized的代码段,大大减少了几率。
synchronized (LazyDoubleCheckSingleton.class) {
if (null==instance) {//第二个if(instance==null),则是为了防止可能出现多个实例的情况。
instance=new LazyDoubleCheckSingleton();
/*
* 1.分配内存给这对象
* 2.初始化对象
* 3.设置instance指向刚刚分配的内存空间(执行完这步 instance才是非 null了)
* 其中2和3会指令重排序,执行顺序可能为123或132
* */
}
}
}
return instance;
}
}
上述代码有几处我们需要分析一下
-
i. 双重检查的含义
代码中我们使用了两处if (null==instance)空判断。第一个 if(instance==null),其实是为了解决代码中的效率问题,只有instance为null的时候,才进入synchronized的代码段,大大减少了执行synchronized的几率。第二个if(instance==null),则是为了防止可能出现多个实例的情况。 -
ii. volatile 关键字修饰instance
为何要使用volatile 关键字修饰instance,网上这篇文章讲的很详细。
简单来说,Java类初始化过程(本例中instance=new LazyDoubleCheckSingleton();)是非原子操作,会经历三个阶段,
1.分配内存给这个对象,
2.初始化对象,
3.设置instance指向刚刚分配的内存空间(执行完这步 instance才是非 null了)。其中2和3会指令重排序,执行顺序可能为123或132。所以当如果执行顺序是132,那么当线程执行完3,此时对象已经不为空,但是并没有初始化。此时另一个线程抢占了CPU,那么执行getInstance()方法时就会直接返回instance(非 null 但却没有初始化),后续使用就会报错了。
解决的方案就是使用volatile关键字修饰。此处他的作用主要是限制指令重排序。PS:有另一种说法是下面这样,希望知道的小伙伴告诉一下。
volatile阻止的不是singleton = new Singleton()这句话内部[1-2-3]的指令重排,而是保证了在一个写操作([1-2-3])完成之前,不会调用读操作(if (instance == null))。
修订
在《java并发编程的艺术》一书中,明确说明了,此处使用volatile关键字修饰的作用就是限制指令重排序(保证时序为123)。
网友评论