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()这句代码的时候,就不会发生指令重排了,也不会出现异常。

  • 相关阅读:
    461. Hamming Distance
    342. Power of Four
    326. Power of Three
    368. Largest Divisible Subset java solutions
    95. Unique Binary Search Trees II java solutions
    303. Range Sum Query
    160. Intersection of Two Linked Lists java solutions
    88. Merge Sorted Array java solutions
    67. Add Binary java solutions
    14. Longest Common Prefix java solutions
  • 原文地址:https://www.cnblogs.com/QicongLiang/p/13627591.html
Copyright © 2011-2022 走看看