• volatile原理和应用场景


    volatile是java语言中的一个关键字,常用于并发编程,有两个重要的特点:具有可见性,java虚拟机实现会为其满足Happens before原则;不具备原子性.用法是修饰变量,如:volatile int i.

    volatile原理

    介绍其可见性先从cpu,cpu缓存和内存的关系入手.

    cpu缓存是一种加速手段,cpu查找数据时会先从缓存中查找,如果不存在会从内存中查找,所以如果缓存中数据和内存中数据不一致,cpu处理数据的一致性就无法保证.从机器语言角度来讲,有一些一致性协议来保证缓存一致,但是本文主要从抽象角度解释volatile为何能保证可见性.对于volatile变量的赋值,会刷入主内存,并且通知其他cpu核心,你们缓存中的数据无效了,这样所有cpu核心再想对该volatile变量操作首先会从主内存中重新拉取值.这就保证了对于cpu操作的数据是最新.

    但是这并不能保证volatile修饰的变量的原子性.让我们想想一个场景,变量volatile int count存储在内存中,cpu核心1和cpu核心2同时读取该数据,并存入缓存,然后进行count++操作.count++实际可以分解为三步:

    int tmp = count;
    tmp = count + 1;
    count = tmp;
    

    count = tmp执行结束,cpu会把count刷入内存并通知其他cpu缓存无效,如果两个cpu核心同时将其刷入了内存,通知了缓存无效,那么我们是不是只得到了count = 2,是不是丢失了一个+1的值.所以不要试图用volatile保证多步操作的原子性,原子性可以通过synchronized进行维护.

    需要注意一点,long类型和double类型的数据长度是64位的,JVM规范允许对于64位类型数据分开赋值,即高位32位和低位32位可以分开赋值,对于这种情况可以使用volatile修饰保证其赋值是一次完成的.但是!!!虽然JVM是这样规定的,绝大多数虚拟机还是实现了64位数据赋值的原子性,即使不使用volatile关键字进行修饰也不会出现读取到只赋值一半的64位类型数据,所以不必要每个longdouble变量之前添加volatile关键字.

    感受一下volatile

    了解完原理,来通过一段代码感受下volatile.

    public class Volatile implements Runnable{
        //自增变量i
        public /*volatile*/ int i = 0;
        @Override
        public void run() {
            while (true){
                i++; //不断自增
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Volatile vt = new Volatile();
            Watcher watcher = new Watcher();
            watcher.v = vt;
            Thread t1 = new Thread(vt);
            Thread t2 = new Thread(watcher);
            t1.start();
            t2.start();
            Thread.sleep(10);
            //打印 i  和 s
            System.out.println("Volatile.i = " + vt.i + "
    watcher.w  = " + watcher.monitor);
            System.exit(0);
        }
    }
    class Watcher implements Runnable{
        public  Volatile v;
    
        public  int monitor;
        @Override
        public void run() {
            while (true){
                monitor = v.i;//不断将v.i的值赋给s
            }
        }
    }
    
    // 这是未加volatile修饰的输出
    Volatile.i = 2517483
    watcher.w  = 1047805
    // 打开volatile注释的输出结果
    Volatile.i = 332754
    watcher.w  = 333354    
    

    第一个输出中未加volatile修饰的i的值和watcher读取的值相差太远,

    第二个输出中相差就不多了.并且i的值比未加volatile关键字的值差很多,说明对volatile变量的赋值消耗会大一些,不过不用在意,我们很少对volatile关键字进行不断自增操作,一般都是作为状态或者保证对象完整性,而且volatilesynchronized轻量太多了,如果只为了保证可见性,volatile一定是最优选.

    哪些场景使用volatile

    状态变量

    由于boolean的赋值是原子性的,所以volatile布尔变量作为多线程停止标志还简单有效的.

    class Machine{
        volatile boolean stopped = false;
    
        void stop(){stopped = true;}
    }
    

    对象完整发布

    这里要提到单例对象的双重检查锁,对象完整发布也依赖于happens before原则,有兴趣可以自己去查阅,这个原则是比较啰嗦,可以简单理解为我满足happens before,那么我之前的代码按顺序执行.

    public class Singleton {
        //单例对象
        private static Singleton instance = null;
        //私有化构造器,避免外部通过构造器构造对象
        private Singleton(){}
        //这是静态工厂方法,用来产生对象
        public static Singleton getInstance(){
            if(instance ==null){
            //同步锁防止多次new对象
                synchronized (Singleton.class){
                //锁内非空判断也是为了防止创建多个对象
                    if(instance == null){
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    

    这是一个会产生bug的双重检查锁代码,instance = new Singleton()并不是一步完成的,他被分为这几步:

    1.分配对象空间;
    2.初始化对象;
    3.设置instance指向刚刚分配的地址。
    

    下面图中,线程A红色先获得锁,B黄色后进入.

    这种情况会出现bug,但是由于volatile满足happens before原则,所以会等对象实例化之后再对地址赋值,我们需要将private static Singleton instance = null;改成private static volatile Singleton instance = null;即可.

    其实还有几种场景,如果想了解更多建议阅读IBM的技术社区的文章https://www.ibm.com/developerworks/cn/java/j-jtp06197.html

  • 相关阅读:
    iOS开发——C篇&数组与指针
    iOS开发——C篇&动态内存分配
    助教工作总结
    助教周报(第二轮)— 王茹瑶,林泽龙,范青青
    助教周报(第一轮)---王茹瑶
    UML总结--熊熊的奇幻旅程
    今天是一个睡前的小故事(是谁要给的大饼)
    我与oracle的爱恨情仇(第一章)
    第三次结对作业(王茹瑶+柯智腾)
    第二次结对作业(王茹瑶+柯智腾)
  • 原文地址:https://www.cnblogs.com/krcys/p/9385360.html
Copyright © 2020-2023  润新知