预备知识
- 可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
- 重排序:如果在单线程下对于指令的重排不会影响逻辑,那么在可以提高运行效率的前提下会进行适当的指令重排。
- 缓存行:CPU中的缓存是分段的,一段对应一个存储空间,称为缓存行,缓存行是CPU缓存中可分配的最小存储单元。
- CPU缓存:为了解决CPU运算速度和内存读取速度不一致问题,CPU加入了缓存机制(L1、L2、L3),CPU通过系统总线、IO桥、内存总线从主内存中读取数据到CPU缓存中,此后将会从缓存中读取数据,进行运算然后刷新缓存。如果是单核CPU没什么问题,但在多核CPU下,各个核对自己缓存中的数据进行修改,但其它核并不知道,会造成各个核中的缓存不一致。
- 嗅探技术:每个核都可以”嗅探”到其它核缓存中共享变量的状态(MESI)、以及对缓存和主存的读写操作。
- 缓存一致性协议
- 实现一:BusLocking(总线锁):通过对总线进行加锁。当一个核对其缓存中的变量进行操作时就会对总线发一个Lock#信号,其它核收到该信号(嗅探技术)就不会继续操作自己缓存中的该变量,当操作结束释放锁以后其它核就会重新从主内存中读取该变量到缓存中。 缺点:总线锁会降低性能。
- 实现二:MESI:通过在缓存行上设置状态标志位。在MESI协议中,每个核缓存对应的缓存控制器不仅知道自己的读写操作,而且也监听(snoop)其它核对其缓存的读写操作,并根据其操作修改自己缓存标志位。
当前状态 | 说明 |
---|---|
M | 当前核缓存的变量与主内存不一致 |
E | 缓存的变量只存在当前核 |
S | 各个核中缓存的变量处于一致状态 |
I | 当前核中缓存的变量无效 |
- 举个例子:当核1中缓存的变量状态从S变为M,同时核2通过嗅探技术,嗅探到此操作,然后会使其缓存行标志位变为I,当核2再读取对应数据时,会判断其它核中缓存的此数据的标志位,发现核1的缓存行标志位是M,则会触发将核1中的缓存行刷新到主内存中,然后核2从主内存中读取到缓存中。
- Lock汇编指令:
- 将当前核中的对缓存的修改刷新到主内存中
- 会锁定当前缓存,将当前缓存中的数据回写到主存中。并通过缓存一致性协议保证操作是原子操作。
- 使其它核中的缓存无效
- 回写到主存中的操作会导致其它核中缓存的失效
- 此指令相当于一个内存屏障,保证不会将此指令之前的操作和此指令之后的操作和此指令进行重新排序。
- 将当前核中的对缓存的修改刷新到主内存中
volatile
概述
- volatile用来修饰变量,使得变量具有可见性。同时保证对此变量的操作指令不会发生重排序
- 对于volatile变量的操作没有相关的加锁操作,性能会比synchronized要高。
- 不保证原子性
原理
- 可见性
- 对于volatile变量的写操作会生成lock汇编指令,导致将缓存回写到主存中,并且使其它线程的工作内存(对应到CPU核的缓存)的此变量失效(就是MESI协议里的Invalid状态)
- 比如在线程A,线程B工作内存中缓存着主内存中的变量a(对应底层的cpu核缓存的状态就是a的状态为S)当线程A将工作内存中a = 1,此时线程B中的a失效,当线程B再次读取a的时候需要从主内存中重新读取。
- 对于volatile变量的写操作会生成lock汇编指令,导致将缓存回写到主存中,并且使其它线程的工作内存(对应到CPU核的缓存)的此变量失效(就是MESI协议里的Invalid状态)
- 有序性
- 执行lock汇编指令时会保证它之前的操作已经执行完毕,并且对其后的操作可见,即只有执行完了lock指令才会执行后续的操作。
- 不保证原子性
- 反证:假如核1,核2都将共享变量a(初始为0)读到缓存进行修改,核1将a++,核2将a += 3,此时核1将缓存中的a修改为1,导致核2缓存里的a变为Invalid。当核2回写缓存a = 3的时候,发现a所在缓存行状态是Invalid,导致核2需要从主存中读 a = 1,此时会触发核1的缓存中的a = 1刷新到主存中,然后核2从内存中读取a = 1并修改自己的缓存中的a = 1,并没有和预期的a最终变为4相符,即不保证原子性。 后续就是核1的对a的缓存状态变为I,核2变为M
应用场景
- 不与其它状态变量共同参与不变约束(比如volatile int a;while(a > b) 而b就是其它状态变量)
- 对此变量的写操作不依赖与当前值(比如a = 1就是不依赖当前值,a = a + 1就依赖了当前值)
应用例子
- 作为状态变量,因为它的可见性保证当它修改以后 其他线程可以及时看到。
1 | public class Main { |
- 懒汉式单例模式双重检测(Double Check)
1 | public class Main { |
- 不严格的读写锁策略
读的时候并没有排斥写,适用更新不频繁的情况。
1 | public class Main { |
参考
- http://www.infoq.com/cn/articles/ftf-java-volatile#anch134390
- https://en.wikipedia.org/wiki/MESI_protocol
- https://www.ibm.com/developerworks/cn/java/j-jtp06197.html
- https://crowhawk.github.io/2018/02/10/volatile/
- https://www.cnblogs.com/xrq730/p/7048693.html
- https://read.douban.com/ebook/15233695/
- http://www.ixirong.com/2015/08/22/java-volatile/