我对--volatile关键字的理解

310 阅读4分钟

这是我参与更文挑战的第9天,活动详情查看: 更文挑战

一、CPU cache模型和JMM模型(Java memory mode)

要想知道volatile关键字的作用,我们得首先清楚,硬件cpu缓存模型和java内存模型的关系:

1.Cpu cache 模型:

在这里插入图片描述 缓存速度:寄存器>L1>L2>L3>主内存。主内存中放入的是共享的一些数据,cpu从主内存中读取数据到三级缓存再到寄存器修改,之后把修改后的数据刷新到主内存中。这个时候我们思考,如果是双CPU,都要修改同一个数据,是不是会引发数据安全的问题呢?通常的解决办法是通过总线BUS或EMSI协议,总线BUS的粒度要比EMSI大,耗费的资源高。

简单解释一下EMSI协议:E(Exclusive 独有的)、M(Modified 修改的)、S(Share 共享的)、I(Invalid 无效的);各个缓存中有一个叫Cache line的有这四种状态,当Cache line为E和S的状态,缓存中的数据和主内存的数据是一致的,是clean的数据、当Cache line为独有时表示,数据只在当前的缓存中存在,Cache line 为共享的时候,表示在别的缓存中也存在。当Cache line 为M的状态,表示在本地缓存中已经修改,其他缓存会有一个监听机制,Cache line会变成I无效。当修改数据刷新到主内存后,其他线程的Cache line又会变为E或者S。

2.Java内存模型

在这里插入图片描述首先声明java内存模型是一个抽象的概念,它并不在物理空间上存在。它的设计思想和CPU cache模型类似,如果要修改一个数据,线程会先到工作空间去找有没有该数据,如果没有就去主内存中找,读到工作空间后,再修改值,再刷新到主内存中。当然,聪明的你会想到数据安全的问题。

3.java内存模型和cpu cache模型的关系:

在这里插入图片描述工作空间和主内存的数据都可能来自Cpu cache模型中寄存器,缓存,主内存。

二、volatile的作用:

1.volatile是用来修饰变量的:

private static volatile int init_value = 0;

volatile具有防止指令重排的作用,他属于happens before原则中的一种。简单来说,jvm会对我们写的代码进行优化,代码的执行顺序可能会被打乱,加了volatile,在volatile之前的变量和之后的变量执行顺序都不能被改变,因此他具有有序性。此外他还保证了数据的可见性,当一个线程修改了某个数据时,其他线程会嗅探到这个数据被改变了,也就是说这个数据被改变了对其他线程可见。

我们可以通过hsdis和jitwatch得到汇编代码:

......
  0x0000000002951563: and    $0xffffffffffffff87,%rdi
  0x0000000002951567: je     0x00000000029515f8
  0x000000000295156d: test   $0x7,%rdi
  0x0000000002951574: jne    0x00000000029515bd
  0x0000000002951576: test   $0x300,%rdi
  0x000000000295157d: jne    0x000000000295159c
  0x000000000295157f: and    $0x37f,%rax
  0x0000000002951586: mov    %rax,%rdi
  0x0000000002951589: or     %r15,%rdi
  0x000000000295158c: lock cmpxchg %rdi,(%rdx)  //在 volatile 修饰的共享变量进行写操作的时候会多出 lock 前缀的指令
  0x0000000002951591: jne    0x0000000002951a15
  0x0000000002951597: jmpq   0x00000000029515f8
  0x000000000295159c: mov    0x8(%rdx),%edi
  0x000000000295159f: shl    $0x3,%rdi
  0x00000000029515a3: mov    0xa8(%rdi),%rdi
  0x00000000029515aa: or     %r15,%rdi
......

三、volatile的运用场景:

1.利用可见性做读写分离:

//volatile保证了数据的可见性
public class VolatileDemo {
    private static final int MAX = 5;
    private static volatile int init_value = 0;

    public static void main(String[] args) {
        //读
        new Thread(()->{
            int localValue = init_value;
            while (localValue<MAX){
                if (localValue != init_value){
                    System.out.printf("init_value is update to %d \n",init_value);
                    localValue = init_value;
                }
            }
        },"Reader").start();

        //写
        new Thread(()->{
            int localValue = init_value;
            while (localValue<MAX){
                System.out.printf("init_value will change to %d \n",(++init_value));
                localValue =init_value;
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"Writer").start();
    }
}

结果如下:

init_value will change to 1 
init_value is update to 1 
init_value will change to 2 
init_value is update to 2 
init_value will change to 3 
init_value is update to 3 
init_value will change to 4 
init_value is update to 4 
init_value will change to 5 
init_value is update to 5 

2.利用可见性做开关

//volatile的使用场景2,做开关用
public class ShutdownDemo extends Thread{

    private volatile static boolean flag = true;

    @Override
    public  void run() {
        doWork();
    }

    private  void doWork() {
        while (flag){
            System.out.println("I am working.....");
        }
    }

    private void shutDown(){
        try {
            TimeUnit.SECONDS.sleep(2);
            flag = false;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        ShutdownDemo shutdownDemo = new ShutdownDemo();
        //这里运行的是run方法
        new Thread(shutdownDemo).start();
        new Thread(shutdownDemo::shutDown).start();
    }

运行结果:2s后停止工作

I am working.....
I am working.....
I am working.....
I am working.....
I am working.....

Process finished with exit code 0

使用场景 3:DCL(double check lock)单例模式

public class DCLDemo {

    private volatile static DCLDemo dclDemo;

    private static DCLDemo getDclDemo(){
        if (dclDemo==null){
            synchronized (DCLDemo.class){
                if (dclDemo==null){
                   dclDemo = new DCLDemo();
                }
            }
        }
        return dclDemo;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()-> System.out.println(getDclDemo())).start();
        }

    }
}

PS:优雅的懒加载:(静态内部类,当然还有枚举等方式也不错)

public class Singleton {  
    static class SingletonHolder {  
        static Singleton instance = new Singleton();  
    }  
      
    public static Singleton getInstance(){  
        return SingletonHolder.instance;  
    }  
}

四、volatile的缺点:

无法保证原子性,要保证原子性,请看我写的另外一篇文章,synchronize的理解:

//volatile关键字并不能保证原子性
public class VolatileIncreaseDemo {
    private static volatile int i =0;

    private static void increase(){
        i++;
    }

    public static void main(String[] args) {
        for (int j = 0; j < 10; j++) {
            new Thread(()->{
                for (int k = 0; k < 5; k++) {
                    increase();
                }
            }).start();
        }
        System.out.println(i);
    }
}

运行结果是小于50的:

20

Process finished with exit code 0