本文介绍Java设计模式第六种
这种模式是在第五种模式的基础之上演变而来,常被成为DCL(double check lock)单例模式,同时,在静态变量上面加上了volatile关键字(一定要加),这个关键字一般来说有两个作用: 内存可见和禁止指令重排序。
首先,我们来假设不加这个关键字会出现什么问题。假设两个线程同时运行到第一个if (instance == null)这段代码,此时假设线程1抢到了锁,那么它可以进行下面的同步代码块,在同步代码块里面,它又检查了一次实例是否为null,那么此时instance肯定为null,它顺利的执行了实例化代码,并且释放了锁对象,此时线程2得以执行。它执行到代码块里面的if (instance == null)会发生什么呢?
我们假设使用的多核CPU,接下来分析一下两个线程的内存模型
从图中可以看出,假如两个线程不再同一个cache中,他们获取的对象都是主存里面的对象的一个复制,那么判断instance肯定是为空的,但是加入了volatile关键字之后,通过MESI缓存一致性协议之后,可以保证在任何一个cache当中的同一个数据发生改变之后,会通知其他cache里面的数据的副本失效,让它们重新从主存里面去获取数据。这就是内存的可见性所起到的作用。那么禁止指令重排序,这里又有什么作用呢?
原来,编译器在进行编译的时候,有可能会将一些指令进行重新排序,以达到性能优化的目的。但是此时假如代码执行到instance = new Student06();时,线程1将instance进行实例化,此时在栈中分配了instance这个引用,同时让这个引用指向了堆中的一个对象。此时如果线程2执行到了它上面的判断instance是否为null的时候,instance肯定是不为null的,因此可以跳过这一步,拿到的就是线程1分配的那个instance引用。但是这个instance在堆中,有可能只是开辟了空间,并没有完成数据的初始化。
代码部分
package singleton;
public class Student06 {
private Student06(){
}
//一定要加上volatile关键字,不然在编译阶段进行指令优化的时候可能会进行指令重排序
//也就是执行new 对象的时候,先返回了对象的引用,此时返回的引用对象还是null;
//也就是第二次检查的时候,发现install还是null,那么就又new 了多个,但是加上了volatile关键字之后
//会告诉虚拟机,禁止指令重新排序
public static volatile Student06 instance;
public static Student06 getInstance() {
if (instance == null) {
synchronized (Student01.class) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (instance == null) {
instance = new Student06();
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Student06.getInstance().hashCode());
}
}).start();
}
}
}
对象的初始化以及指令重排分析
我写了一个类用来分析java生成的字节码
package singleton;
/**
* @Author: micro cloud fly
* @Description: 测试字节码文件
* @Date: Created in 4:39 下午 2020/11/2
*/
public class ByteCode {
int a=10;
public static void main(String[] args) {
ByteCode byteCode = new ByteCode();
}
}
生成的字节码如下
0 new #3
3 dup
4 invokespecial #4 >
7 astore_1
8 return
来分析一下这段字节码
0代表调用new这个方法的时候,此时,会在堆中分配一块内存,此时成员变量a为int类型的初始值0
4代表调用构造函数init方法的时候,将对象初始化,此时a=8,堆中的对象初始化完毕
7代表将byteCode这个引用指向堆中的这个对象
什么时候会重新排序呢?这个要看编译器,比如JIT编译器在发现上一行的代码和下一行的代码没有任何关系,在单线程中不会影响程序的执行的时候,就会执行,比如上面的分析中,执行的顺序可能是074,那么执行到7的时候,byteCode已经不是null了,此时a=0,返回的对象的成员变量a=0,那么它和我们需要的对象中的a=8的就不是同一个对象,因为它只是一个半初始化化的对象,所以hashcode值也不相同了。
加上violate关键字之后,jvm使用了内存屏障,对于临界区的资源禁止指令重排序,所以不会发生类似的问题