volatile的几个灵魂拷问
要想讲清楚volatile关键字,这时候就应该主动从内存模型开始讲起,然后说原子性、可见性、有序性的理解,铺垫好这些才是到volatile关键字的原理,假定前面一篇内存模型的文章已经理解到位了,那么这里直接从volatile的原理开搞!
首先说结论,volatile关键字作用的是保证可见性和有序性,并不保证原子性。只有synchronized关键字同时保证上述三种特性。但是volatile是轻量级的synchronized,它在多线程中保证了变量的“可见性”。可见性的意思是当一个线程修改了一个变量的值后,另外的线程能够读取到这个变量修改后的值。
1、保证可见性
用 volatile 关键字修饰的共享变量,编译成字节码后增加 Lock 前缀指令,该指令要做两件事:
- 将当前工作内存缓存行的数据立即写回到主内存。
- 写回主内存的操作会使其他工作内存里缓存了该共享变量地址的数据无效(缓存一致性协议保证的操作)。
对volatile修饰的变量,执行写操作的话,JVM会发送一条lock前缀指令给CPU,CPU在计算完之后会立即将这个值写回主内存,同时因为有MESI缓存一致性协议,所以各个CPU都会对总线进行嗅探,自己本地缓存中的数据是否被别人修改。
如果发现别人修改了某个缓存的数据,那么CPU就会将自己本地缓存的数据过期掉,然后这个CPU上执行的线程在读取那个变量的时候,就会从主内存重新加载最新的数据了。
所以,volatile的可见性就是:lock前缀指令 + MESI缓存一致性协议
Java程序打印成汇编语句,引用自 https://zhuanlan.zhihu.com/p/770856952、保证有序性
Lock 前缀指令有内存屏障的作用。上一篇文章已经提到过了,JMM一共有4种内存屏障,分别是 LoadLoad、LoadStore、StoreStore、StoreLoad。JMM 给volatile插入内存屏障保守策略:
- 在每个 volatile 写操作的前面插入一个 StoreStore 屏障,后面插入一个 StoreLoad 屏障。
- 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障和一个 LoadStore 屏障。
StoreStore屏障可以保证在volatile写之前,前面所有的普通读写操作同步到主内存中
StoreLoad屏障可以保证防止前面的volatile写和后面有可能出现的volatile度/写进行重排序
LoadLoad屏障可以保证防止下面的普通读操作和上面的volatile读进行重排序
LoadStore屏障可以保存防止下面的普通写操作和上面的volatile读进行重排序
3、不具备原子性
volatile不具备原子性,多个线程去写同一个公共volatile修饰的变量会出现线程安全问题,对于64位的long和double,如果没有被volatile修饰,那么对其操作可以不是原子的。在操作的时候,可以分成两步,每次对32位操作。如果使用volatile修饰long和double,那么其读写都是原子操作,这里可以稍稍注意下。
由于volatile不具备原子性,因此还需要有一个原子性能力的补充,这里就可以看看这个文章进行回顾下:《CAS是什么?Atomic包知多少?》
4、和Synchronized的内存屏障差别
按照可见性保障来划分。内存屏障可分为加载屏障(Load Barrier)和存储屏障(Store Barrier)。加载屏障的作用是刷新处理器缓存,存储屏障的作用冲刷处理器缓存。Java虚拟机会在 MonitorExit ( 释放锁 ) 对应的机器码指令之后插入一个存储屏障,这就保障了写线程在释放锁之前在临界区中对共享变量所做的更新对读线程的执行处理器来说是可同步的。相应地,Java 虚拟机会在 MonitorEnter ( 申请锁 ) 对应的机器码指令之后临界区开始之前的地方插入一个加载屏障,这使得读线程的执行处理器能够将写线程对相应共享变量所做的更新从其他处理器同步到该处理器的高速缓存中。因此,可见性的保障是通过写线程和读线程成对地使用存储屏障和加载屏障实现的。
按照有序性保障来划分,内存屏障可以分为获取屏障(Acquire Barrier)和释放屏障 ( Release Barrier )。获 取 屏 障 的 使 用 方 式 是 在 一 个 读 操 作 ( 包括 Read-Modify-Write 以及普通的读操作 )之后插入该内存屏障,其作用是禁止该读操作与其后的任何读写操作之间进行重排序,这相当于在进行后续操作之前先要获得相应共享数据的所有权 ( 这也是该屏障的名称来源 )。释放屏障的使用方式是在一个写操作之前插入该内存屏障,其作用是禁止该写操作与其前面的任何读写操作之间进行重排序。这相当于在对相应共享数据操作结束后释放所有权( 这也是该屏障的名称来源 )。 Java虚拟机会在 MonitorEnter( 它包含了读操作 ) 对应的机器码指令之后临界区开始之前的地方插入一个获取屏障,并在临界区结束之后 MonitorExit ( 它包含了写操作 ) 对应的机器码指令之前的地方插入一个释放屏障。因此,这两种屏障就像是三明治的两层面包片把火腿夹住一样把临界区中的代码(指令序列)包括起来:
可以发现,与volatile类似,synchronized底层也是通过释放屏障和获取屏障的配对使用保障有序性,加载屏障和存储屏障的配对使用保障可见性。最后又通过锁的排他性保障了原子性。