Volatile关键字详解

volatile是Java中一个非常重要的关键字,要想完全搞懂volatile的作用,需要一些额外的辅助知识。

要了解volatile,就要先对CPU,CPU cache和主存有所有了解。

主存就是我们平时说的内存,它的读写速度很慢,而CPU运算速度很快,为了弥补二者的差异,在CPU和主存之间引入了CPU Cache,
现在很多CPU都会有三层Cache,分别称为L1,L2和L3,其中L1最靠近CPU,缓存速度也最快,L1这块缓存被分为了两个部分,一部分是指令缓存,称为L1i,一部分是数据缓存,称为L1d.

只有有缓存,就会出现缓存不一致的问题,比如i++这个操作。

在有缓存时候的计算流程是:

  1. 从主存中读取i的值到缓存中
  2. 在缓存中把i的值加1
  3. 把缓存中的值刷回到主存中。

这样的模型在单线程下没有任何问题,但是在多线程中,就会出现问题,比如在步骤二中执行,此时有一个线程直接给i重新赋值了,这时候就会出现问题。

解决这个问题的一个有效办法就是 缓存一致性协议,这个协议大体思路是:

  1. 当CPU操作Cache中的副本时,会去查看该副本是不是在其他Cache中也存在一份
  2. 如果不存在的话,CPU就可以对Cached中的副本进行任意的操作了
  3. 如果存在的话, CPU先把Cached中的副本读取到自己的寄存器中,然后通知其他的CPU说"你们保存的Cache无效了"
  4. 其他CPU收到这个通知之后,如果要操作这个变量的副本,就会从主存里在读取一遍。

那么我们接下来看一下Java的内存模型,Java的内存模型一个很重要的作用就是规定了:一个线程对共享变量(主内存中的变量)的修改,何时对其他线程可见。

具体来说就是:

  1. 每个线程都有自己的工作内存,称为缓存。
  2. 线程只能操作自己的工作内存,不能直接操作主内存
  3. 共享变量在工作内存中有一个副本
  4. JVM决定何时把工作内存中的副本刷回主内存。

接下来了解一下并发编程必须要掌握的几个概念:

  1. 原子性
    一组操作要么不做,要么保证全部做完,不会被中断
    volatile关键字无法保证原子性

  2. 可见性
    一个线程对一个变量进行了修改,另一个线程可以马上感知到这种修改。
    volatile关键字可以保证可见性

  3. 有序性
    主要就是防止进行指令重排,因为为了优化执行顺序,JVM并不一定会按照书写的顺序去执行,会进行优化,优化的原则是:不保证执行的顺序,只保证重排之后的执行结果和重排之前一样。

在多线程情况下,指令重排有时候会造成问题。所以有时候要禁止掉指令重排。

最常见的就是通过一个变量来进行初始化判断,比如

private boolean isInit = false;
private Manager mManger;
public Manager getManager(){
  if(!isInit){
    mManger = initManager();
    isInit = true;
  }
  return mManager;
}

在上面那个例子中,单线程情况下不会有任何问题,在多线程情况下,如果不进行指令重排也不会有问题。
但是如果进行了指令重排,比如指令重排之后,把isInit = true放在了mManager = initManager()之上,很可能在多线程的情况下出现mManager为空的情况,从而出现空指针异常。而且这种异常还很难发现,通常大家都是一脸懵逼,说mManager肯定不可能为空呀。

那现在我们把多线程情况下的指令重排造成的mManager为空的情况说一下。

线程1执行getManager方法,由于指令重排,isInit = true先执行,然后去真正初始化mManager,最后返回mManager实例,不会有任何问题。
但是当线程1执行完isInit = true之后,线程2开始执行getManager方法,发现isInit为true,就直接返回了mManager,而此时mManager还没有初始化,所以线程2中会出现空指针异常。

所以建议, 依赖一个变量来判断是否初始化,而且这种判断是运行在多线程下的,那么该变量一定要是volatile的

接下来我们看一下JVM是如何通过内存模型来保证原子性,可见性和有序性的。

JVM保证对于变量的读取和直接赋值都是原子性的,什么是直接赋值呢,比如x = 1这就是直接赋值,JVM保证其原子性。但是y = x就不是原子性的,因为这涉及到三个操作,第一步是把x的值读入工作内存,第二步是把x的值赋给y,第三步是把y的值刷回主内存。
同理类似于i++,x=x+1都不是原子性操作。

如果要确保这些操作的原子性,都要使用synchronized关键字。

JVM通过synchronized关键字,volatile关键字和显示锁Lock来保证可见性。

JVM通过synchronized关键字,volatile关键字和显示锁Lock来保证有序性。

可见volatile关键字只能保证可见性和有序性。而使用锁的话则三个特性都能保证。

volatile的使用场景

由于volatile关键字可以保证可见性和有序性,并不能保证原子性,所以它不能完全替代锁。我们工作中对volatile的使用也是利用了它的可见性和有序性。

  1. 修饰开关变量。比如我们上面提到的例子
  2. 修饰单例变量,防止在单例模式中由于指令重排生成多个变量。

volatile关键字不会导致阻塞,但是使用锁会导致阻塞

你可能感兴趣的:(Volatile关键字详解)