Java中的volatile关键字详解及单例模式双检锁问题分析
【参考文献】http://www.cnblogs.com/dolphin0520/p/3920373.html
看了好多关于volatile关键字的文章,这篇应该是讲得最清楚的了吧,从Java内存模型出发,结合并发编程中的原子性、可见性、有序性三个角度分析了volatile所起的作用,并从汇编角度大致说了volatile的原理,说明了该关键字的应用场景;我在这补充一点,分析下volatile是怎么在单例模式中避免双检锁出现的问题的。
一、先总结下并发编程中3个条件:
1、原子性:要实现原子性方式较多,可用synchronized、lock加锁,AtomicInteger等,但volatile关键字是无法保证原子性的;
2、可见性:要实现可见性,也可用synchronized、lock,volatile关键字可用来保证可见性;
3、有序性:要避免指令重排序,synchronized、lock作用的代码块自然是有序执行的,volatile关键字有效的禁止了指令重排序,实现了程序执行的有序性;
二、单例设计模式中的双检锁形式
public class Singleton{
private static Singleton instance; //声明静态的单例对象的变量
private Singleton(){} //私有构造方法
public static Singleton getInstance(){ //外部通过此方法可以获取对象
if(instance== null){
synchronized (Singleton.class) { //保证了同一时间只能只能有一个对象访问此同步块
if(instance == null){
instance = new Singleton();
}
}
}
return instance; //返回创建好的对象
}
}
双检锁方式避免了懒汉式加重量级锁synchronized,看似一种非常聪明的做法,但是这种写法在某些时候会出现错误,具体分析如下:
在创建对象时,执行instance=new Singleton()语句时,考虑执行下面的代码:
mem = allocate(); //Allocate memory for Singleton object.
instance = mem; //Note that instance is now non-null, but has not been initialized.
ctorSingleton(instance); //Invoke constructor for Singleton passing instance.
step1: 先申请一块存储空间step2: 将引用指向存储空间
step3: 调用构造器初始化该空间
以上执行顺序是完全可能出现的,在理想情况下,按照1->3->2的步骤执行时,双检锁形式可以正常工作,但是由于Java内存模型,允许重排序,所以完全可能按照1->2->3的顺序执行,则导致双检锁形式出现问题。即线程1在执行single=new Singleton()语句时,刚好按照1->2->3的顺序执行到step2处,此时,instance指向mem内存区域,而该内存区域未被初始化;同时,线程2在第一个if(instance==null)地方发现instance不为null了,于是得到这个为被初始化的实例instance进行使用,导致错误。因此,双检锁的单例模式成了学术实践而已。
三、双检锁单例模式的升级---采用volatile关键字
双检锁模式的问题是由于初始化对象时指令重排序锁导致的,而volatile关键字正好可以禁止指令重排序,因此,考虑将volatile关键字应用于单例模式中,便可完美解决双检锁的问题,让双检锁方案变得可行:
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; //返回创建好的对象
}
}
综上, volatile修饰instance,使得在初始化instance时,保证按step1->3->2的顺序执行,不会出现单纯双检锁时出现的问题。