哈喽大家好,我是IT老哥,今天我们来讲讲面试必问的voliate
单线程的情况下呢,我们肯定用不到这个voliate
只有在多线程的情景下才能用到,文章结尾我会举一个经典的案例
voliate三特性
保证可见性;
不保证复合操作的原子性;
禁止指令重排。
第一:可见性
先给大家介绍一下JMM的内存模型
我们定义的共享变量就是存在主内存中,每个线程内的变量是在工作内存中操作的,当一个线程A修改了主内存里的一个共享变量,这个时候线程B是不知道这个值已经修改了,因为线程之间的工作内存是互相不可见的
那么这个时候voliate的作用就是让A、B线程可以互相感知到对方对共享变量的修改,当线程A更新了共享数据,会将数据刷回到主内存中,而线程B每次去读共享数据时去主内存中读取,这样就保证了线程之间的可见性
这种保证内存可见性的机制是:内存屏障(memory barrier)
内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。
内存屏障有两个作用:
1.阻止屏障两侧的指令重排序;
2.强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。
这里不深入说这个机制了,对于大家目前的情况可能理解起来比较困难
第二:不保证复合操作的原子性
1、什么叫原子性?
所谓原子性,就是说一个操作不可被分割或加塞,要么全部执行,要么全不执行。
i = 0; ---1
j = i ; ---2
i++; ---3
i = j + 1; ---4
上面四个操作,有哪个几个是原子操作,那几个不是?如果不是很理解,可能会认为都是原子性操作,其实只有1才是原子操作,其余均不是。
1---在Java中,对基本数据类型的变量和赋值操作都是原子性操作;
2---包含了两个操作:读取i,将i值赋值给j
3---包含了三个操作:读取i值、i + 1 、将+1结果赋值给i;
4---同三一样
Java只保证了基本数据类型的变量和赋值操作才是原子性的(注:在32位的JDK环境下,对64位数据的读取不是原子性操作*,如long、double)
第三:有序性(禁止jvm对代码进行重排序)
有序性:即程序执行的顺序按照代码的先后顺序执行。
一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:
在单线程环境下不能改变程序运行的结果;
存在数据依赖关系的不允许重排序
第四:举个非常常见的voliate用法—DCL
什么是DCL呢,其实就是double check lock的简写
DCL很多人都在单利中用过,如下这种写法:
public class Singleton {
private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){ // 1
synchronized (Singleton.class){ // 2
if(singleton == null){ // 3
singleton = new Singleton(); // 4
}
}
}
return singleton;
}
}
表面上这个代码看起来很完美,但是其实有问题
先说一下他完美的一面吧:
1、如果检查第一个singleton不为null,则不需要执行下面的加锁动作,极大提高了程序的性能;
2、如果第一个singleton为null,即使有多个线程同一时间判断,但是由于synchronized的存在,只会有一个线程能够创建对象;
3、当第一个获取锁的线程创建完成后singleton对象后,其他的在第二次判断singleton一定不会为null,则直接返回已经创建好的singleton对象;
但是到底是哪里有错误呢,听老哥细细分析
首先创建一个对象分为三个步骤:
1、分配内存空间
2、初始化对象
3、讲内存空间的地址赋值给对象的引用
但是上面我讲了,jvm可能会对代码进行重排序,所以2和3可能会颠倒,
就会变成 1 —> 3 —> 2的过程,
那么当第一个线程A抢到锁执行初始化对象时,发生了代码重排序,3和2颠倒了,这个时候对象对象还没初始化,但是对象的引用已经不为空了,
所以当第二个线程B遇到第一个if判断时不为空,这个时候就会直接返回对象,但此时A线程还没执行完步骤2(初始化对象)。就会造成线程B其实是拿到一个空的对象。造成空指针问题。
解决方案:
既然上面的问题是由于jvm对代码重排序造成的,那我们禁止重排序不就好了吗?
voliate刚好可以禁止重排序,所以改造后的代码如下:
public class Singleton {
//通过volatile关键字来确保安全
private volatile static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
这样就不会存在2和3颠倒的问题了
解决方案二:
基于类初始化
该解决方案的根本就在于:利用classloder的机制来保证初始化instance时只有一个线程。JVM在类初始化阶段会获取一个锁,这个锁可以同步多个线程对同一个类的初始化。
public class Singleton {
private static class SingletonHolder{
public static Singleton singleton = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.singleton;
}
}
这种解决方案的实质是:允许步骤2和步骤3重排序,但是不允许其他线程看见。
晚安,兄弟们!
给个[在看],是对IT老哥最大的支持