Fork me on GitHub

单例模式的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 }
  1. 为什么需要两次check?
    1. 如果没有外层的check,相当于给整个getInstance()方法加上了synchronized关键字,也就是每次获取单例对象都要获取class对象的monitor,monitor是粒度较大的的锁,开销较大。所以外层的判断目的是:第一次获取单例对象后,再次获取该单例对象无需进行同步
    2. 如果没有内层的check,假如有两个线程,线程1和线程2同时进入外层判断,即第8步,线程1获得对象锁,进入同步代码块并初始化对象后,释放对象锁,返回单例对象结束了,线程1获取对象锁进入同步代码块后又再次初始化了instance对象,导致多线程下单例模式的非线程安全;
  2. 为什么instance实例需要加volatile关键字?
  3. 因为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的规则是:对一个变量的写操作的结果,对这个变量的读操作可见

参考链接:

  1. https://www.cnblogs.com/dolphin0520/p/3920373.html
  2. https://stackoverflow.com/questions/7855700/why-is-volatile-used-in-this-example-of-double-checked-locking
  3. https://blog.csdn.net/DL88250/article/details/5439024
  4. https://www.cnblogs.com/dolphin0520/p/3613043.html

 

posted @ 2018-08-01 19:57  gitmoji  阅读(2187)  评论(2编辑  收藏  举报