浅谈volatile

Posted ccsert

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈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。

操作系统级别解决指令重排序是基于锁总线和缓存一致性协议。

在深就不往下谈了,讲到这里已经比较深了。

以上是关于浅谈volatile的主要内容,如果未能解决你的问题,请参考以下文章

浅谈volatile与计算机缓存一致性协议之间的联系

浅谈Mybatis

浅谈AngularJS中的$parse和$eval

从内部入手,浅谈malloc和new的区别

编写安全代码:小心volatile的原子性误解

Java基础:volatile详解