多线程之volatile

84 阅读4分钟

volatile关键字

  • 用来修饰可在不同线程访问和修改的变量
  • 无法修饰方法、代码块、方法参数、局部变量、常量

CPU高速缓存

  1. 程序运行时数据是存储在内存中的,而程序的执行工作由CPU来完成
  2. CPU的发展是遵循摩尔定律的(即价格不变,集成电路可容纳元器件数目每隔18-24个月便会增加一倍,性能也将提升一倍),因此CPU的性能会越来越好。若每次都先去内存读取数据,程序执行效率并不会提升,因此厂商为解决这一问题,引入了CPU高速缓存功能
  3. 即在CPU中缓存一份内存数据,频繁获取时无需去内存获取,大大提高CPU效率。当需修改数据时可先修改CPU缓存中数据,等运算结束后再回写至内存中。这就会引入新的问题
  4. 当前设备大都是多核CPU(即CPU存在多个运算单元),在单线程场景中使用CPU高速缓存无任何问题,但程序大都需要在多线程场景中运行,所以就引入了可见性问题(即线程B中修改变量a,对线程A不会立即可见)

可见性

volatile保证了不同线程对共享变量操作时的可见性,即一个线程修改,其他线程立即可见

  1. 未使用volatile修饰
  • 在线程2中修改线程1终止条件,线程1不会立即可见(即终止线程1),而会读取其在CPU缓存的原值
  1. 使用volatile修饰变量
  • 指示JVM,该共享变量不稳定,每次使用直接读取主存中值

原理

  • 生成底层汇编指令时,会对volatile修饰共享变量增加Lock前缀指令,Lock前缀指令会锁定当前内存区域(缓存行)并将当前缓存行数据立即回写至主内存
  • CPU缓存回写到内存时会通过MESI协议(Modified,Exclusive,Shared,Invalid高速缓存一致性协议)向其他CPU广播一条消息使其他缓存了该变量的地址失效,使用时需重新到内存中获取

MESI协议

  • Modified:缓存行已被修改,还未写入主内存,此时只能有一个CPU独占该修改状态
  • Exclusive:缓存行与主内存一致,且为主内存唯一拷贝,此时只能有一个CPU独占该状态
  • Shared:此高速缓存行可能存储在计算机的其他高速缓存中,且与主内存一致,此时各CPU均可对该数据读取但不能写入
  • Invalid:缓存行失效,不能使用

有序性

程序执行的顺序要按照代码的先后顺序

  • 系统为充分利用缓存,提高程序执行速度,编译器在底层执行时会进行指令重排序操作,引入"有序性"问题
  • volatile通过禁止编译器、CPU指令重排序和部分happens-before规则,解决有序性问题

原理

  1. 内存屏障(内存栅栏,是一个CPU指令)

声明为volatile后,变量进行读写操作时,会通过插入特定的"内存屏障"方式禁用指令重排序

写内存屏障

  • volatile变量在进行写操作时,会在变量的前后插入StoreStore屏障,确保本次写操作前所有普通写操作均已完成;接着在写操作后插入StoreLoad屏障,强制所有后来读写操作在本次操作后进行,既保证了其他线程对变量立即可见

读内存屏障

  • volatile变量在进行读操作时,会在读操作前插入LoadLoad屏障,确保本次读操作前所有读操作均已完成;接着在读操作后插入LoadStore屏障,防止本次读操作后的写操作被重排序到读之前,即保证了读取时总是最新的写入值
StoreStore:禁止之前的普通写和之后的volatile写重排序
StoreLoad:禁止之前的volatile写和之后的volatile读/写重排序
LoadLoad:禁止之后所有的普通读操作和之前的volatile读重排序
LoadStore:禁止之后所有普通写操作和之前volatile读重排序

原子性

对volatile变量单次读/写操作可以保证原子性,如double和long类型变量,但不能保证i++这种操作的原子性,i++本质上是读、写两次操作