深入探究 volatile


volatile介绍

刚说了synchronized 关键字,那就肯定一定要说一下volatile关键字,关于volatile我是非常”痛恨”的,因为这个关键字导致去年春招的一次面试失败了,所以还是要好好看看这个关键字,深入的探究一下.
大部分人都知道volatile是保证了可见性和禁止重排序, 具体怎么保证,我们就好好说一下

实现原理

Java内存模型

Java的内存模型大致的内容就是说,每个线程会将共享变量从主内存中拷贝到工作内存,然后在各个线程内部对数据进行操作.但是问题就是,每个线程的持有的共享变量只是一份拷贝,和主内存中的变量的值并不是实时一致的,而工作内存何时将操作后的结果写回主内存却没有人知道,这在多线程的情况下,就会发生数据不一致和线程不安全的问题

volatile实现原理

将视角移到计算机指令的层面,根据volatile的描述,在生成汇编代码的时候,被volatile描述的变量在写操作的指定前会被添加lock前缀(使用javap 可以看到), 这个lock指定会做什么呢?
再看操作系统的层面,每个CPU都有自己的缓存,CPU会将内存中的数据保存到内部缓存中,这个内部缓存是个大学问,需要好好再另说. 和Java内存模型一样,我们并不知道什么时候缓存中的数据会被写回内存,而这个lock指令就会指定将缓存中的数据写回系统内存,那么问题又来了,你一个线程写回了,但是其他缓存中可能还有旧的数据,这就需要操作系统出马了,操作系统为了保证缓存一致性,每个处理器会通过嗅探在总线上传播的数据来检查自己的缓存是不是过期了,当发现自己缓存行的对应的内存地址呗修改,就会将缓存视为无效状态,当处理器对这个数据进行操作的时候,就会从内存中重新读取.
在著名的happens-before规则中有一条: 对一个volatile变量的写,happens-before于任何对volatile的读,happens-before, 有人翻译为发生前,但是真正的意思应该是 对..可见 ,上面的话也就是对一个volatile变量的写,对于任何对volatile的读 可见

可见性

上面说到了底层是怎么样的,但是仔细一看是有问题的,我们说volatile保证可见性和有序性,但是不保证原子性,但是从我们上面说的,每一个线程修改共享变量都会使其他线程所在cpu的缓存失效,那咋一看不是保证了原子性吗?那最经典的自增操作来看,
自增操作i++分为三个部分:

  1. 取出i值
  2. 令i = i + 1
  3. 写回i的值

volatile可以保证写到缓存中的值可以马上写回主内存,同时使其他cpu的缓存失效,既然每个线程可以实时看到最新的值,那每次取到的i值都是最新的,操作不就没什么问题了吗?
这个问题困扰了我很久,也就是我之前说的面试遇到的问题,解决这个问题还是对于计算机组成不熟悉,我们知道缓存是为了解决CPU计算和读取内存之间的速度差异存在的,但是CPU还有寄存器的存在,寄存器存放暂时的指令,数据,位址信息,我们的线程切换保存上下文就有寄存器的信息,这下就清楚了,volatile无法保证原子性就是因为CPU操作的是寄存器中信息,而寄存器无法和内存和缓存同步,这才导致的线程不一致

有序性

上面说了volatile如何保证可见性,下面说一下有序性的相关.
重排序已经不是什么秘密了, 这属于处理器优化,JMM允许处理器会将指令按照适合自己的执行顺序进行排序,如果是单线程的情况下,是没有什么问题的(重排序原则就是不改变单线程下的运行结果),但是在多线程的情况,一切都说不一定了.
所以出现了为了实现volatile的禁止重排序,有内存屏障的概念,java编译器会在生成指令的适当位置插入内存屏蔽来禁止特定类型的处理器重排序,有四个屏障指令:

  • StoreStore屏障:禁止上面的普通写和下面的volatile写重排序;
  • StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序
  • LoadLoad屏障:禁止下面所有的普通读操作和上面的volatile读重排序
  • LoadStore屏障:禁止下面所有的普通写操作和上面的volatile读重排序

参考
《Java 并发编程的艺术》


  目录