zoukankan      html  css  js  c++  java
  • java基础----volatile

    一.volatile是什么

      如果用一句话概括volatile的话,那volatile其实就是java虚拟机提供的轻量级的同步机制。它具有一下三个特点:

      1.保证可见性

      2.不保证原子性(因为不保证原子性,所以他是轻量级的)

      3.禁止指令重排

    二.保证可见性

      首先,我们先看看下面的代码

    import java.util.concurrent.TimeUnit;
    
    class MyData{
        //int testData = 0;
        volatile int testData = 0;
    
        public void addTo60(){
            this.testData = 60;
        }
    }
    
    public class volatileDemo {
        public static void main(String[] args) {
            MyData myData = new MyData();
    
            //工作线程
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "	 come in");
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                myData.addTo60();
                System.out.println(Thread.currentThread().getName() + "	 update number value: " + myData.testData);
            }, "test").start();
    
            while (myData.testData == 0)
            {
                //假如不使用volatile修饰变量
                //这里main线程会一直在这里等待
                //因为工作线程修改了变量,却不能对main线程可见,main线程会一直拿着初始值的副本,也就是0
            }
    
            System.out.println(Thread.currentThread().getName() + "	 mission is over");
        }
    }

    关于volatile是怎么实现可见性的,可以总结为以下两点:

      1.从主内存到工作内存<读>:每次使用变量前  先从主内存中刷新最新的值到工作内存,用于保证能看见其他现场对变量修改的最新值。

      2.从工作内存到主内存<写>:每次修改变量后必须立刻同步到主内存中,用于保证其他线程可以看到自己对变量的修改。(因为cpu比内存的速度要快很多,因此这中间有一个高速缓存的概念,一般修改后的变量都是先存进高速缓存,然后再刷新到主内存中)

      3.指令重排序:保证代码的执行顺序和程序的执行顺序一致。(并发环境下 代码的执行顺序与程序的执行顺序有时并不一致,会出现串行的现象固有指令重排序优化一说。JAVA1.5之后彻底修复了这个BUG在用volatile变量的时)

    三.不保证原子性

      volatile有个比较不好的地方就是它不保证原子性,按照惯例,下面还是先上一段代码

    import java.util.concurrent.TimeUnit;
    
    class MyData{
        volatile int testData = 0;
    
        public void add(){
            this.testData ++;
        }
    }
    
    public class volatileDemo {
        public static void main(String[] args) {
            MyData myData = new MyData();
    
            for (int i = 0; i < 20; i++){
                new Thread(() -> {
                    for (int j = 0; j < 1000; j++){
                        myData.add();
                    }
                },"test"+i).start();
            }
    
         //后台默认有main线程和gc线程,因此这里选择当线程数量大于2时候,main线程退出暂停。 while (Thread.activeCount() > 2){ Thread.yield(); }
         //这里每次运行出来的结果都是不同的 System.out.println(Thread.currentThread().getName() + " final number is " + myData.testData); } }

       上述代码运行出来的结果不是我们想要的结果,导致这种情况出现的原因是,在并发编程中,可能存到两个线程同时去主内存中拿到变量,然后进行计算操作,例如线程a、b,同时拿到主内存中的变量x1到自己的工作内存里面,然后计算后得出同样的x2,当要把x2写回主内存时,其中一个会将数据刷新到主内存中,而另一个线程处于挂起状态,当前一个线程写入操作完成后,后一个线程被唤醒,在还没获取变量最新值的时候,立即进行写入操作(写覆盖),这时候主内存中的变量被刷新了两次,但是它的数值只增加了1,因为两个线程算出来的结果时一样的,这时候就会导致变量最后自增的结果不是我们想要的结果。

      那么我们如何去解决这个问题呢?其实很简单,juc包里面有一个atomic类的数据,我们使用它来作为我们操作的对象就可以了。

    import java.util.concurrent.atomic.AtomicInteger;
    
    class MyData{
        volatile int testData = 0;
    
        public void add(){
            this.testData ++;
        }
    
        volatile AtomicInteger atomicInteger = new AtomicInteger();
    
        public void addAtom(){
            atomicInteger.getAndIncrement();
        }
    }
    
    public class volatileDemo {
        public static void main(String[] args) {
            MyData myData = new MyData();
    
            for (int i = 0; i < 20; i++){
                new Thread(() -> {
                    for (int j = 0; j < 1000; j++){
                        myData.add();
                    }
                },"test"+i).start();
            }
    
            for (int i = 0; i < 20; i++){
                new Thread(() -> {
                    for (int j = 0; j < 1000; j++){
                        myData.addAtom();
                    }
                },"test-atomic"+i).start();
            }
    
            //后台默认有main线程和gc线程,因此这里选择当线程数量大于2时候,main线程退出暂停。
            while (Thread.activeCount() > 2){
                Thread.yield();
            }
    
            //这里每次运行出来的结果都是不同的
            System.out.println(Thread.currentThread().getName() + "	 final number is " + myData.testData);
            System.out.println(Thread.currentThread().getName() + "	 final number is " + myData.atomicInteger);
        }
    }

      可是为什么使用atomic类的对象就可以解决原子性的问题呢?其实atomic是通过CAS来保证他的原子性的。

      那,CAS又是什么呢?简单来说,CAS是compareAndSwap的缩写,意思是对比和交换。

      java中CAS操作依赖于Unsafe类,Unsafe类所有方法都是native的,直接调用操作系统底层资源执行相应任务,它可以像C一样操作内存指针,是非线程安全的。

    //第一个参数o为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值,
    //expected表示期望值,x表示要设置的值,下面3个方法都通过CAS原子指令执行操作。
    public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);                                                                                                   
    public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x); 
    public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);

      而atomicInteger在更新他的value的时候,就是调用了unsafe类中的compareAndSwapInt方法去更新的。

      举个例子,在线程需要对一个变量进行写操作的时候,会先对比这个变量是否符合预期值,如果符合,则会进行写操作,如果不符合,代表有其他线程对这个变量进行了修改,则获取变量当前值作为最新值,返回重新进行计算操作,依次循环,知道在对比的时候符合预期值。例如当前变量x1=1,同时有线程a,b过来获取变量,并进行+1操作,此时线程a计算完毕,对比主内存中变量是否为1,假如是,就将结果2写进主内存中,此时线程b也计算完毕了,对比主内存中的变量值,发现2!=1,意思是有其他线程已经对这个变量进行修改了,就会把2拿回去,重新进行+1的操作,然后再次对比。

    四.禁止指令重排

      计算机在执行程序时,为了提供性能,编译器和处理器常常会对指令进行重排

        1.源代码

        2.编译器优化的重排

        3.指令并行的重排

        4.内存系统的重排

        5.最终执行指令

      单线程环境下,不需要考虑指令重排的情况,程序最终执行的顺序和代码的顺序是一致的。

      处理器在进行指令重排时,必需要考虑指令之间的数据依赖性,例如 Int x = 0; x = x + 5,这样的语句是不能被重排的,因为x需要先初始化。

      多线程环境下,线程交替执行,由于编译器优化重排的存在,实际运行的结果是无法保证与代码里面预期的结果一致的。

      而volatile是通过插入内存屏障,禁止内存屏障前后的指令执行指令重排序优化。

      举个例子,在一个使用了双端检查机制的单实例中,假如不用volatile来修饰实例的话,将会产生预期之外的结果

    public class SingletonDemo {
        private static volatile SingletonDemo instance = null;
    
        private SingletonDemo(){
            System.out.println(Thread.currentThread().getName() + "	 创建用例");
        }
    
        //双端检查机制
        public static SingletonDemo getInstance(){
    
            //这种是常规写法,但是在多线程环境下会有问题
            //if (instance == null){
            //    instance = new SingletonDemo();
            //}
    
            //假如a,b,c线程同时过来,发现instance == null 则会进入锁代码块
            //假如这里不加检查,则无论实例是否初始化好,线程进来都会有一个抢夺锁资源的操作,很浪费
            if(instance == null)
            {
                //因为上锁了,假如a获得锁,b,c只能等着
                synchronized (SingletonDemo.class)
                {
                    //这里之所以要在检查一次,是因为假如不做判断
                    //当a线程初始化了这个实例后,b,c线程相继获取到锁资源然后进来执行锁代码块
                    //如果没有判断,则会直接再执行一次初始化实例
                    //这样明显不符合我们的要求,因此进来的时候需要再判断一次,b,c线程进来后发现instance以及不为null了,就不会初始化实例了
                    if(instance == null)
                    {
                        instance = new SingletonDemo();
                    }
                }
            }
    
            return instance;
        }
    
        public static void main(String[] args) {
            for(int i = 0; i < 10; i++){
                new Thread(()->{
                    System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
                    System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
                    System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
                },"test"+i).start();
            }
    
        }
    }

      假如不在instance那里加上volatile修饰的话,则会有可能有某个线程在调用SingletonDemo.getInstance()的时候,获得一个null值,要想知道这里面的原因,首先我们需要知道在getInstance()方法中,instance = new SingletonDemo()这句代码具体执行了什么操作。

        1.memory = allocate()  //给对象分配内存

        2.instance(memory)   //初始化对象

        3.instance = memory //将instance变量指向对象的内存地址 

        一般来说,都是按照以上顺序执行的,但是因为操作不存在变量依赖,所以在多线程环境下,可能会发生指令重排操作,将会变成

        1.memory = allocate()  //给对象分配内存

        2.instance = memory //将instance变量指向对象的内存地址 

        3.instance(memory)   //初始化对象

      这样就导致一种情况,就是a线程进来后,发现instance==null,就会执行instance = new SingletonDemo()这条代码,然后这时候b线程进来了,而a线程初始化对象已经完成了“分配内存”和“修改变量引用”这两步,但是却还没有来得及执行初始化对象这一步。这时候线程b发现instance!=null,不会进入到锁代码块,会直接return instance,然而,这时候的instance所指向的内存区域,其实还没完成对象的初始化,是空的,这样,就会出现预期之外的异常了。

      因此,用volatile修饰instance,这样在执行instance = new SingletonDemo()这句代码的时候,就不会发生指令重排了,也不会出现异常。

  • 相关阅读:
    CentOS7 安装jdk8
    CentOS7 安装和配置 mysql5.7
    CentOS7 安装和配置Tomcat
    vi编辑器设置行号可见
    前端基础-css(2)
    前端基础-css(1)
    前端基础-html(3)
    前端基础-html(2)
    前端基础-html(1)
    IO多路复用、协程
  • 原文地址:https://www.cnblogs.com/QicongLiang/p/13627591.html
Copyright © 2011-2022 走看看