浅谈volatile
浅谈volatile
这篇文章我们主要了解一下几个问题
- volatile的特性与指令重排序
- DCL单例
- volatile的实现,内存屏障
volatile的特性和指令重排序
首先volatile拥有可见性,这里就不过多解释了
然后另外一点是它能解决指令重排序。
那么问题来了什么是指令冲排序?
通俗的讲
cpu的速度至少比内存快100倍,为了提升效率,会打乱原来的执行顺序,比如先执行指令A,但是A执行的比较慢,那么这个时候可能会直接去执行指令B,B先执行完了,这样B指令可能就排在了A指令前面,但是前提是A指令和B指令没有依赖关系。
在更通俗一点就是cpu执行指令是乱序执行的,他们没有固定的顺序。
按道理说cpu这样做会让程序运行效率更高,为什么我们还要去解决指令重排序不让它乱序执行呢?
这里我们就要先谈谈new一个对象会有哪些过程
这里我借助idea的‘jclasslibs’来查看new一个对象具体干了些什么。
具体的代码如下
public class newObject {
public static void main(String[] args) {
Object o = new Object();
}
}
一段没太大意义的代码
我们看下它具体调用了哪些Java指令
总共5条指令,我们来一一讲解
1.new:在Java堆内存中申请分配内存空间,并将地址压入栈中。
2.dup:复制操作数栈顶的值,然后在压入栈中,这个时候栈顶有两个一样的值。也就是两个一样的对象地址
3.invokespecial:调用初始化方法,这是一个实例方法,它会从操作数栈顶弹出一个this引用,也就是说它会弹出之前入栈的一个对象地址。
4.astore_1:这里的指令本质上是astore_n
,当前帧的局部变量数组的索引,这条指令的具体含义就是,将堆栈顶部的内容存储到局部变量
5.return:返回
我们大致了解了这些指令的含义
我们梳理一下,new一个对象大致分这几步
1.在堆区分配对象需要的内存
2.对所有实例对象赋默认值
3.初始化
4.栈区地址引用
我们知道object的默认值是null,假如说现在第三步和第二步顺序调换了,那么原本我是要返回一个对象的给别人的,结果就会是返回一个空给别人。
这里我们模糊的描述了它大概会发生什么问题,接下来会有实际例子。
DCL单例
DCL单例模式也就是双重检查锁单例,普通的单例有什么问题在这里我们不进行过多讨论,我们这里只谈DCL。
先看下DCL单例的代码
public class DCLSingleton {
private /** volatile */ static DCLSingleton instance = null;
public static DCLSingleton getInstance() {
if(null == instance) { // 线程二检测到instance不为空
synchronized (DCLSingleton.class) {
if(null == instance) {
instance = new DCLSingleton(); // 线程一被指令重排,先执行了赋值,在执行初始化
}
}
}
return instance;
}
}
这里我们具体解释一下
1.此时有两个线程都在调用getInstance(),这个时候instance还为null,此时线程A进来了,第四行代码的if判断会通过,在A线程还没跑完这个时候B线程也进来了,因为A还没到new对象那一步,所以B也会通过第四行代码的判断。
2.这个时候为了保证原子性,所以我们加一把锁,这个时候A在执行B就得等着。
3.A到了第6行代码,判断通过接着成功创建对象,然后释放锁,接着将单例对象返回了。
4.B获得到锁开始执行,但是因为A已经创建了对象此时instance已经不为null,所以判断不通过直接返回单例对象。
这是理想情况,但是假如说发生指令重排序,初始化变量和变量赋默认值的顺序对调了会怎么样
我们直接跳到第三步
A通过第6行代码的判断,在创建对象的时候发生了指令重排序,在还没成功初始化对象的时候就已经把对象赋值给了instance,也就是将默认值赋值给了instance,紧接着它会释放锁,此时还没初始化B线程进来了,发现还为null,那么它也去创建对象,这个时候它创建完以后直接返回,A初始化完了也直接返回,那么这里对象就被创建了两次。
此时它已经不是单例了。
这就是比较实际的指令重排序了,解决方法就是直接把代码里volatile取消注释就好了,其实对于DCL指令重排序有很多解决方法,这里就不展开讨论了。
volatile的实现,内存屏障
volatile是如何解决指令重排序的。
它是通过内存屏障去解决了指令重排序,具体有以下这些内存屏障,这些内存屏障是jvm级别的与操作系统不同。
jvm定义了内存屏障的规范
StoreStoreBarrier
volatile 写操作
StoreLoadBarrier
LoadLoadBarrier
volatile 读操作
LoadStoreBarrier
读写操作被这些内存屏障隔离他们之间不能指令重排
而jvm调用操作系统的实现则是各种各样的缓存一致性协议。
常见有mesi协议,是基于英特尔cpu。
操作系统级别解决指令重排序是基于锁总线和缓存一致性协议。
在深就不往下谈了,讲到这里已经比较深了。