面试官:有用过单例模式吗?
我:有有有(自信满满)。
面试官:说说单例模式几种写法?
我:懒汉式和饿汉式,懒汉式巴拉巴拉,饿汉式巴拉巴拉。
面试官:我们都知道synchronized加锁是比较耗费资源的,你这种写法每次访问都需要获得锁(基础的懒汉式写法),效率比较低,有什么优化的方式吗?
我:沉思片刻,脑海灵光一现。可以采用双重检查加锁的方式,巴拉巴拉。(还好之前看到过,暗自庆幸)
面试官:为什么双重检查加锁需要加volatile关键字?
我:要不我们问问度娘?
在回答这个问题之前我们要明确这几点,一个是对象的创建过程,一个是什么是指令重排,一个是CPU时间片的概念,一个是synchronized不会禁止指令重排,最后一个是volatile禁止指令重排。
2.对象的创建过程对象的创建过程主要分成三步,如下图展示的汇编码所示,主要是0,4,7这三步。
0 这步是为新创建的对象申请内存,但是此时对象中的成员变量的值是默认的值(半初始化),即下图a 的值此时是0;
4 初始化对象,在这步才把10赋给成员变量a
7 建立关联,把testDemo引用和new 出来的TestDemo对象建立关联
public class TestDemo {
private int a = 10;
public static void main(String[] args) {
TestDemo testDemo = new TestDemo();
// 0 new #2 申请内存,半初始化,此时a的值是0(当对象刚new出来的时候会给里面的成员变量设置默认初始值,int类型的初始值是0)
// 3 dup 复制
// 4 invokespecial #3 <company/syncronized/TestDemo.> 初始化,在这步把10赋给a,此时a的值是10
// 7 astore_1 testDemo和new TestDemo()建立关联
// 8 return
}
}
3.指令重排
指令重排是JMM(java内存模型)中的一个概念,它是指计算机在执行程序时,编译器和处理器会对不存在数据依赖性的指令进行重新排序。
什么是数据依赖性:就是A,B两个指令,B指令的执行依赖A指令的执行,举个简单的例子,看下面的代码,语句2需要语句1申明a变量之后才能使用,那么它们之间就存在数据依赖性。
什么是指令重排:一般情况下rely()方法的执行顺序是1,2,3,4顺序执行,但是在编译器和处理器的优化下,执行顺序有可能变成1,3,2,4,也有可能是3,1,2,4,这个就是指令的一个重排。那么有没有可能出现1,4,2,3的情况呢,不会出现,现有3,4存在数据依赖,所以3必须在4之前被执行。
public void rely(){
int a = 10; //语句1
a = a +1 ; //语句2
int b = 8; //语句3
b = b +1; //语句4
}
4.CPU执行时间片
时间片定义:时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片,即该进程允许运行的时间,使各个程序从表面上看是同时进行的。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。而不会造成CPU资源浪费。
产生的问题:根据上面的时间片定义,我们可以得出,CPU不会等待一个进程执行完毕之后在执行下一个进程,而是CPU会给每一个进程一段执行的时间,这段时间结束,就会轮到下一个进程去使用CPU,这样会出现一个上面问题呢?就是我的进程可能执行到一半,时间片时间到了,CPU给下一个进程了,那么我的进程在这个时间片内就只执行了一半,需要等待下次占有CPU的时候才能把全部的进程执行完毕。也就是说,一个进程可能是需要多个CPU执行时间片的时间来完成。
5.指令重排对双重检查加锁模式的影响
下面进入我们的正题。经过刚才的分析,我们知道创建对象是分成三步走,看下图的1,2,3。那么在这个创建过程中,有没有可能会发生2,3指令重排序?答案是肯定的,因为synchronized它是不会禁止指令重排,假设2,3指令发生了顺序交换,也就是test 引用先和 new SingleDemo对象建立连接,然后在初始化new SingleDemo,那么此时问题就产生了。
之前我们提到cpu时间片的概念,那么我这个进程如果正好执行到test引用建立连接,cpu时间片时间到了,然后轮到下一个进程来执行。好的,那么小伙伴最容易困惑的点来了,synchronized不是保证原子性吗?我的线程还没有执行完毕,我还握着这把锁,就算下一个线程进来也是阻塞的状态,不会对我产生影响。只有当我下一个获得cpu,然后执行完毕,释放锁,那么下个线程才能进行操作,那么既然当前线程一定会执行完毕,那么顺序交换也没有影响了吧。是的,如果我当前线程执行完毕,确实没有影响,但是我下一个进程进来不是阻塞呢?
问题的关键来了,我第一个线程A进来之后,new singledemo()过程发生了指令重排,初始化和建立引用联系的顺序换了,我先建立了引用联系,也就是说此时test 引用指向了一个没有初始化,只有半初始化状态的new Singledemo对象,也就是说此时test是不为空的,然后这时候我cpu时间片的时间到了,然后当前线程A让出cpu,但是当前线程A仍然持有锁。我下一个线程B进来之后,在外层if(test==null)条件下进行判断,然后发现我test对象里面是有东西的,然后就直接return了,也就是说线程B已经获得了一个初始化的new Singledemo对象,你线程A锁着就锁着吧,我线程B拿到对象了。那么此时会产生一个什么问题,就是线程B中的成员变量信息是错误的,我本来a=10,你拿到半初始化状态对象的a=0,那么我在使用这个成员变量的时候就是不正确的。
那么怎么解决指令重排序的问题呢?加volatitle关键字,这样可以保证在new singledomo过程中不发生指令重排。
public class SingleDemo {
//private volatile static SingleDemo test =null;
private static SingleDemo test =null;
private int a = 10;
public int getA() {
return a;
}
private SingleDemo(){}
//双重检查加锁模式
public static SingleDemo get2(){
if(test==null) { //外层检查是问题关键
synchronized (SingleDemo.class) {
if (test == null) {
test = new SingleDemo();
//new singleDemo对象,半初始化 //1
//初始化 //2
//test引用 和 new singleDemo对象建立连接 //3
}
}
}
return test;
}
}
beAwesomeToday
原创文章 16获赞 11访问量 1万+
关注
私信
展开阅读全文
作者:beAwesomeToday