zoukankan      html  css  js  c++  java
  • 浅谈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。

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

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

  • 相关阅读:
    MongoDB一键安装
    Mongo基本操作
    MongoDB AUTH结果验证及开启方法
    MongoDB AUTH结果验证
    MongoDB使用
    MongoDB-安装配置
    11204RAC-dbca建库脚本
    MySQL主从同步最佳实践
    实时抓取主从的同步状态
    守护神 Supervisor
  • 原文地址:https://www.cnblogs.com/ccsert/p/12665989.html
Copyright © 2011-2022 走看看