【面试专栏】深入理解volatile

文章同步更新在个人公众号“梓莘”,欢迎大家关注,相互交流。
谈谈对volatile的理解

volatile是java虚拟机提供的轻量级的同步机制(同步比如synchronized)拥有3大特性:

  • 保证可见性

  • 不保证原子性

  • 禁止指令重排

简单来说也就是volatile保证可见和有序,但是并不保证原子性。

要更好的理解这个需要对JMM有个大概的了解,接下来先看下JMM的简单概念。

谈谈JMM(Java Memory Model)

JMM也就是java内存模型,它本身是一种抽象的概念,描述的是一种规则或规范,通过程序中各个变量(实例字段、静态字段和构成数组对象中的元素)的访问方式。

JMM关于同步的规定:

  1. 线程解锁前,必须把共享变量的值刷新回主内存

  2. 线程加锁前,必须读取主内存中的最新值到自己的工作内存

  3. 加锁解锁是同一把锁

由于JMM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要

将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不通的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其访问过程如下图:

在这里插入图片描述

与此对应的JMM有3大特性:

  • 可见性

  • 原子性

  • 有序性

代码验证


package com.zishen;

import java.util.concurrent.TimeUnit;

import java.util.concurrent.atomic.AtomicInteger;

class Data{//Data.java ===> Data.class===>JVM字节码

    /*主内存设置默认为0*/

    //1.1 不添加volatile 一直卡在main处 因为虽然修改了但是没人通知main

    //1.2 添加volatile关键字之后 就可以验证我们的要求

    volatile int number =0;

    public void add(){

        this.number = 10;

    }

    //注意 此时number前面加了volatile关键字的 volatile不保证原子性

    public void addPlus(){

        number++;

    }

    //注意 此时number前面加了volatile关键字的 volatile不保证原子性但是加了synchronized就可以保证

    public synchronized void addPlusSyn(){

        number++;

    }

    AtomicInteger atomicInteger = new AtomicInteger();

    public void atomicAdd(){

        atomicInteger.getAndIncrement();

    }

}

/**

*1、验证volatile的可见性

* 1.1 加入int number=0; number变量之前没有添加volatile关键字修饰

* 1.2 添加了volatile可以解决可见性问题

*

* 2、验证volatile不保证原子性

*  2.1原子性指的是什么

*    不可分割,完整性,也就是某个现场正在做某个业务时,中间不允许插队或分割,需要整体完整要么同时成功,要么同时失败。可以参考数据库中的原子性来理解。

*  2.2 volatile不保证原子性验证

*

*  2.3 字节码解释原因

*

*  2.4 如果解决

*    2.4.1 加sync

*    2.4.2 使用juc下的Atomic

*/

public class VolatileDemo {

    /**

    * volatile可以保证可见性,及时通知其他线程,主物理内存的值已经被修改

    * @param args

    */

    public static void verifySee(String[] args) {

        Data data = new Data();

        new Thread(()->{

          System.out.println(Thread.currentThread().getName()+"\t come in");

            try {

                //暂停一会线程

                TimeUnit.SECONDS.sleep(3);

                data.add();

                System.out.println(Thread.currentThread().getName()+": upate number value"+data.number);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

        },"ZISHEN").start();

        //第二个就是我们的main线程

        while(data.number==0){

          //main线程一直等待,直到number值不再等于0

        }

        System.out.println(Thread.currentThread().getName()+"\t mission is over");

    }

    public static void main(String[] args) {

        Data data = new Data();

        for(int i=0;i<20;i++){

            new Thread(()->{

                for(int j=0;j<100000;j++){

                    data.addPlus();

                    //data.addPlusSyn();

                    data.atomicAdd();

                }

            },String.valueOf(i)).start();

        }

        //需要等待上面20个线程都执行完成之后,再用main线程取得最终的值

        //为啥是2 一个main 一个GC 所以最少是2

        while(Thread.activeCount()>2){

            Thread.yield();

        }

        System.out.println(Thread.currentThread().getName()+"int type,finally number value "+data.number);

        System.out.println(Thread.currentThread().getName()+"atomicinteger type,finally number value "+data.atomicInteger);

    }

}

通过代码的演示我们可以看到对主存中共享变量的操作都是各个线程拷贝到自己额工作内存进行操作后再写回到主内存中,这就可能存在一个线程A修改了共享变量X的值但还未写回主内存时,另外一个线程B又对主内存中同一个共享变量X进行操作,但此时A线程工作内存中共享变量X对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题。

为了更好的了解后面所说到的指令重排我们通过一个简单的自加操作的字节码来分析下。

T1.java


package com.zishen;

/**

* @ClassName T1

* @Description TODO

* Author zishen

* @Date 2019/12/24 20:15

* @Version 1.0

**/

public class T1 {

    volatile  int a=1;

    public void add(){

        a++;

    }

}

通过javap命令编译为字节码内容如下:


Compiled from "T1.java"

public class com.zishen.T1 {

  volatile int a;

  public com.zishen.T1();

    Code:

      0: aload_0                            //从局部变量0中装载引用类型的值

      1: invokespecial #1                  // Method java/lang/Object."":()V

      4: aload_0

      5: iconst_1

      6: putfield      #2                  // Field a:I

      9: return

  public void add();

    Code:

      0: aload_0

      1: dup                              //

      2: getfield      #2                  // Field a:I

      5: iconst_1

      6: iadd

      7: putfield      #2                  // Field a:I

      10: return

}

话不多说,上图:

在这里插入图片描述

我们可以看到++操作实际上是分成了3个指令执行的,所以会出现写丢失的情况,也就是写覆盖。

指令重排

计算机在执行时,为了提高性能,编译器和处理器常常会对指令做重排,分三种:

在这里插入图片描述

单线程环境里面确保程序最终执行结果和代码顺序执行的一致性。

处理器在进行重排序时必须考虑指令之间的数据依赖性。

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

理解这个需要对内存屏障有一个简单的了解。

内存屏障

内存屏障(Memory Barrier)又叫内存栅栏,是一个CPU指令,他的作用有2个:

  1. 一是保证特定操作的顺序执行

  2. 二是保证某些变量的内存可见性

由于编译器个处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。

内存屏障另外一个作用的强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

volatile实现了禁止指令重排优化,从而避免了多线程环境下程序出现乱序执行的现象。

在这里插入图片描述

线程的安全性如何获得保证

1、工作内存与主内存同步延迟现象导致的可见性问题

可以使用synchronized或volatile关键字解决,他们都可以使一个线程修改后的变量立即对其他线程可见

2、对于指令重排导致的可见性问题和有序性问题

可以利用volatile关键性解决,因为volatile的另外一个作用就是禁止重排序优化。

哪些情况下使用到了volatile?

1、单例模式DCL代码

package com.zishen;

/**

* @ClassName SingletonDemo

* @Description单例模式

* Author zishen

* @Date 2019/12/24 21:34

* @Version 1.0

**/

public class SingletonDemo {

    private static SingletonDemo instance= null;

    private SingletonDemo(){

        System.out.println(Thread.currentThread().getName()+"\t 我是构造方法");

    }

    //原始模式

    public synchronized  static SingletonDemo getInstance(){

        if(instance == null){

            instance = new SingletonDemo();

        }

        return instance;

    }

    //DCL Double Check Lock 双端检测机制

    public  static SingletonDemo getDCLInstance(){

        if(instance == null){

            //同步代码块

            synchronized (SingletonDemo.class) {

                if (instance == null) {

                    instance = new SingletonDemo();

                }

            }

        }

        return instance;

    }

    public static void main(String[] args) {

        //System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());

        ///System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());

      // System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());

        for(int i=0;i<10;i++){

            new Thread(()->{

                //SingletonDemo.getInstance();

                SingletonDemo.getDCLInstance();

            },String.valueOf(i)).start();

        }

    }

}

DCL机制不一定线程安全,原因是有指令重排序的存在,加入volatile可以禁止指令重排。原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。instance = new SingletonDemo();

可以分为以下3步完成。


memory=allocate();//1、分配对象内存空间

instance(memory);//2、初始化对象

instance=memory;//3、设置instance指向刚分配的内存地址,此时instance!=null

因为步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后的执行结果在单线程中并没有改变。因此这种重排优化是允许的。


memory=allocate();//1、分配对象内存空间

instance=memory;//3、设置instance指向刚分配的内存地址,此时instance!=null 但是对象还没有初始化完成。

instance(memory);//2、初始化对象

但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关系多线程间的语义一致性。所以当一条指令访问instance不为null时,由于instance实例已初始化完成,也就造成了线程安全问题。

2、单例模式volatile分析

package com.zishen;

/**

* @ClassName SingletonVolatileDemo

* @Description volatile禁止重排的单例模式

* Author zishen

* @Date 2019/12/24 21:57

* @Version 1.0

**/

public class SingletonVolatileDemo {

    //在这加volatile禁止指令重排

    private static volatile SingletonVolatileDemo instance= null;

    private SingletonVolatileDemo(){

        System.out.println(Thread.currentThread().getName()+"\t 我是构造方法");

    }

    //原始模式

    public synchronized  static SingletonVolatileDemo getInstance(){

        if(instance == null){

            instance = new SingletonVolatileDemo();

        }

        return instance;

    }

    //DCL Double Check Lock 双端检测机制

    public  static SingletonVolatileDemo getDCLVolatileInstance(){

        if(instance == null){

            //同步代码块

            synchronized (SingletonVolatileDemo.class) {

                if (instance == null) {

                    instance = new SingletonVolatileDemo();

                }

            }

        }

        return instance;

    }

    public static void main(String[] args) {

        //System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());

        ///System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());

        // System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());

        for(int i=0;i<10;i++){

            new Thread(()->{

                SingletonVolatileDemo.getDCLVolatileInstance();

            },String.valueOf(i)).start();

        }

    }

}

书写不易,转载请注明出处哦~

你可能感兴趣的:(【面试专栏】深入理解volatile)