单例模式的double check写法中的volatile关键字
在多线程环境中,volatile能保证共享变量的可见性以及一定程度的有序性。单例模式有多种写法,有线程安全的和非线程安全的,有懒汉式和饿汉式,有利用static关键字修饰变量、方法、代码块、内部类的实现,还有用枚举实现的,今天我们讨论下单例模式里面较为复杂的double check写法,先看下代码:
1 public class Singleton{ 2 private static volatile Singleton instance = null; 3 4 private Singleton(){} 5 6 public Singleton getInstance(){ 7 if(null == instance){ 8 synchronized(Singleton.class){ 9 if(null == instance){ 10 instance = new Singleton(); 11 } 12 } 13 } 14 return instance; 15 } 16 }
- 为什么需要两次check?
- 如果没有外层的check,相当于给整个getInstance()方法加上了synchronized关键字,也就是每次获取单例对象都要获取class对象的monitor,monitor是粒度较大的的锁,开销较大。所以外层的判断目的是:第一次获取单例对象后,再次获取该单例对象无需进行同步
- 如果没有内层的check,假如有两个线程,线程1和线程2同时进入外层判断,即第8步,线程1获得对象锁,进入同步代码块并初始化对象后,释放对象锁,返回单例对象结束了,线程1获取对象锁进入同步代码块后又再次初始化了instance对象,导致多线程下单例模式的非线程安全;
- 为什么instance实例需要加volatile关键字?
- 因为volatile的禁止指令重排序,在第10步中,初始化instance对象并非原子操作,它包括:1.开辟堆内存2.调用构造方法初始化对象3.将instance指向新对象;如果没有volatile关键字,且在并发情况下,如果某个线程完成了1 2两个步骤,还未给instance变量赋值,此时另一个线程进入外层判空后后发现instance对象非空,就返回了未构造完全的instance对象,导致空指针异常;volatile的意义在于能够禁止对当前对象进行指令的重排序,也就是happen-before原则的关于volatile的一条:"volatile变量规则:对一个变量的写操作happen before于后面对这个变量的读操作",也就是说,无论什么情况,对于volatile变量的写操作必须在完成后才能读取,不能暴露写操作的中间状态。所以不会出现未完成构造就读取的情况;但是volatile不能保证同时对变量的写操作也是有序的,也就是volatile不能保证原子性
happen before:描述了线程安全的可见性,有很多规则,关于volatile的规则是:对一个变量的写操作的结果,对这个变量的读操作可见
参考链接:
- https://www.cnblogs.com/dolphin0520/p/3920373.html
- https://stackoverflow.com/questions/7855700/why-is-volatile-used-in-this-example-of-double-checked-locking
- https://blog.csdn.net/DL88250/article/details/5439024
- https://www.cnblogs.com/dolphin0520/p/3613043.html