前言
在之前的单例模式的博客中,我对常见的几种单例模式做了介绍,但是对于其中一种,双检锁实现的单例模式而言,其实并不是简单的几行代码那么简单,其中蕴含的深意其实还是很重要的,在这里,我对其做一些解释
--------------------------------------------------------------------------------------------
(一)看似完美的双检锁单例模式
public class Person { private static Person person; private Person(){} public static Person getInstance(){ if(person == null){//第一次检查 synchronized (Person.class){//加锁 if(person == null)//第二次检查 person = new Person();//标注一下,这里其实存在问题 } } return person; } }
由于双重检查存在,去降低了同步的开销,从而看似很完美,解决了在并发环境下的缺点。但是注意下,我标注的那一行代码,其实是存在一个叫 重排序 的问题。
这一行代码可以变成下面的三行伪代码
1. memory = allocate() //分配对象的内存空间
2. ctorInstance(memory) //初始化对象
3. person = memory //设置person指向刚分配的内存地址
上面三行代码其实2-3之间,是存在一个重排序的问题的,有可能会出现,这种情况分两种情况讨论一下
①单线程下的
1. memory = allocate() //分配对象的内存空间
2. person = memory //设置person指向刚分配的内存地址
3. ctorInstance(memory) //初始化对象
如果是这个样子的话,其实是不会影响最终结果的,但是当多线程情况下就不一样了
②多线程下的
时间 | 线程A | 线程B |
t1 | 1.分配对象的内存空间 | |
t2 | 2.设置person内存指向 | |
t3 | 1.判断person是否为空 | |
t4 | 2.由于person不为空,所以线程B要访问person引用的对象 | |
t5 | 3.初始化对象 | |
t6 | 4.访问person指向的对象 |
这样来看,线程B会访问到一个还没有初始化的对象,那么问题就暴露了出来
所以,为了解决这个问题,我们可以想到两个解决方法
1.不允许 2与3重排序
2.允许2与3重排序,但是不允许其他线程看到这个重排序
所以针对第一点,我们有了上面双检锁的单例模式的升级版
(二)仅仅加了一个关键字的双检锁单例模式
public class Person { private static volatile Person person; private Person(){} public static Person getInstance(){ if(person == null){ synchronized (Person.class){ if(person == null) person = new Person(); } } return person; } }
只加了一个volatile关键字就把问题解决了,当声明对象为volatile后,上面三行代码之间的重排序在多线程的环境中,就被禁止了。
volatile关键字的特性有两个:
1.保证可见性,不保证原子性
a.当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去
b.这个写会操作会导致其他线程中的缓存无效。
2.禁止指令重排
重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。
由此可见,仅仅一个关键字,就解决了这个问题
----------------------------------------------------------------------------------------
上面的方法对重排序的问题解决方法,采用了第一种,那么对于第二种,我们可以采取之前我写的那种采用静态内部类的方法去解决,代码可以去之前的博客去看
思路就是:允许2-3行代码重排序,但是不允许非构造线程(另一个线程)看到这个重排序;由JVM内部的锁机制来保证不会创建多个实例,非常巧妙的避开了多线程的问题。