volatile底层原理分析

一、CPU多核缓存架构模型

1.1、早期的计算机CPU架构模型

  • CPU与硬盘中数据的交互

数据存储在硬盘中,CPU需要将硬盘的数据加载到主内存(RAM)中,CPU运算是与主内存(RAM)进行数据交互的

  • CPU与硬盘中数据的交互的特点

CPU与主内存(RAM)直接进行交互,CPU的运算速度远远高于主内存(RAM)的运算速度,CPU运算速度连年增加,主内存(RAM)一直没有太大的变化,CPU的运算速度受限于主内存(RAM)

volatile底层原理分析

1.2、现在的计算机CPU多核缓存架构模型

  • CPU与硬盘中数据的交互

首先将硬盘中的数据加载到主内存(RAM)中,然后将数据加载到CPU高速缓存中,最后CPU运行时实际上是CPU与CPU高速缓存进行数据交互的,CPU高速缓存的运行速度介于CPU与主内存(RAM)之间。CPU高速缓存的价格昂贵,还没有运行的数据放在主内存(RAM)中,正在运行的数据会放在先从主内存(RAM)中读取,并保留一份在CPU高速缓存中,下次读取数据的时候直接从CPU高速缓存中读取,加快CPU的执行效率

  • CPU与硬盘中数据的交互的特点

现在的计算机中的CPU运算在CPU与主内存(RAM)之间加了一层CPU的高速缓存,运算速度快、效率高。但是这种CPU多核缓存架构模型也带来了问题,最典型的就是缓存不一致的问题(即其中一个CPU高速缓存中修改了对不同CPU高速缓存之间不可见导致缓存不一致问题)

volatile底层原理分析
1.2.1、内存间交互操作

read(读取):从主内存中读取数据

load(载入):将主内存读取到的数据写入工作内存

use(使用):从工作内存中读取数据来计算

assign(赋值):将计算好的值重新赋值到工作内存中

store(存储):将工作内存数据写入到主内存中

write(写入):将store过去的变量赋值给主内存中的变量

lock(锁定):将主内存变量加锁,标识为线程独占状态

unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量

举例:

当共享变量a初始值为0时,线程A修改共享变量a的值为1时所涉及的内存间的交互操作

1.2.2、缓存不一致的解决方式

CPU多核缓存架构模型缓存不一致的问题是硬件架构本身的问题

1.2.1.1、总线加锁

CPU从主内存(RAM)读取数据到CPU高速缓存中,会在总线这个数据加锁,这样其他CPU没法去读或写这个数据,直到这个CPU使用完数据释放锁之后其他CPU才能读取该数据

1.2.1.2、MESI缓存一致性协议 And CPU总线嗅探机制
  • MESI缓存一致性协议

多个CPU从主内存读取同一个数据到各自的高速缓存,当其中某个CPU修改了缓存里的数据,该数据会马上同步回主内存,其他CPU通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效

MESI缓存一致性协议对应共享变量有四个状态:

M(修改):该CPU高速缓存中此共享变量修改过值
E(独占):只有该CPU高速缓存中含有此共享变量
S(共享):有多个CPU高速缓存中含有此共享变量
I(无效):CPU高速缓存中的该共享变量无效

  • CPU总线嗅探机制

CPU监听总线中当某一个共享变量修改了值,并经过了总线,在其他CPU运行的线程通过CPU总线嗅探机制来监听并让线程中对应的共享变量变为失效状态

二、JMM(Java Memory Model,Java内存模型)

JMM并不是真实存在的,是CPU多核缓存架构模型的抽象,CPU多核缓存模型是真实物理存在的

volatile底层原理分析

线程之间的共享变量都存在主内存(RAM)中,每个线程都有自己私有的的工作内存,线程私有的工作内存中会存储一份共享变量的副本,线程真正运行时交互的共享变量是工作内存空间中的共享变量副本,这样的Java线程内存模型和CPU内存模型很类似

2.1、JMM存在的问题

  • 可见性问题

描述:

在多线程环境中,其中一个变量修改了共享变量的值,但是对其他的线程并不可见。比如线程A修改了a变量的值,但是修改完a变量之后,并没有立马将修改a变量的值write到主内存(RAM)中和在线程A修改之前就read了a变量的其他线程使用的还是a变量修改之前的值

解决方式:

使用volatile修饰共享变量或者加锁

  • 竞争问题

描述:

在多线程环境中,不同线程同时修改同一个共享变量,例如共享变量a初始值为0,两个线程都对这个共享变量做+1操作,如果这两个线程是并行执行的话,那么共享变量在两个线程执行完毕后a的值可能为1,也可能为2。为1主要是因为当后一个线程在前一个执行的线程未执行完时从主内存(RAM)中read到共享变量的值并load到线程私有的工作内存中,这个时候共享变量a的值还是初始值为0;两个线程执行完后都将a=1write到主内存(RAM)中

解决方式:

​ 加锁

三、volatile底层原理

volatile能够保证可见性和有序性,不能够保证原子性,保证原子性需要借助synchronized这样的锁机制

3.1、并发编程的三大特性

  • 可见性

同一个共享变量对于不同线程来说是可见的,即其中一个线程修改了共享变量的值,其他的线程能够立刻就对其修改的值可见

  • 原子性

即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行

  • 有序性

程序执行的顺序按照代码的先后顺序执行。在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性

3.2、volatile保证可见性和有序性

  • happens-before原则

Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

使用volatile修饰的共享变量,总线会开启MESI缓存一致性协议以及CPU总线嗅探机制来解决JMM缓存一致性问题,也就是共享变量在多线程中可见性的问题。底层实现主要是通过汇编lock前缀指令,共享变量加了lock前缀指令,在线程修改完共享变量后,它会马上执行storewrite操作。在执行write操作经过总线时,其他的CPU上运行的线程根据CPU总线嗅探机制会修改其共享变量为I(失效状态),在执行store操作前,会先执行lock操作,然后再执行store操作,接着执行write操作,最后执行unlock操作

lock前缀指令(实际上相当于一个内存屏障,也称内存栅栏)的含义:

  1. 会将当前处理器缓存行的数据立即写回到系统内存
  2. 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效(MESI协议)
  3. 确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成

store操作之前对主内存的共享变量lock,在write结束原子性后unlock,锁的粒度很小很小,几乎忽略不计

volatile底层原理分析

四、volatile关键字的应用场景

与synchronized相比

  1. volatile不会让线程阻塞,响应速度比synchronized快
  2. volatile只能修饰变量,synchronized能在变量、方法、代码块、类上使用
  3. volatile不会造成线程的阻塞,synchronized可能造成线程的阻塞

通常来说,使用volatile必须同时具备以下2个条件

  1. 对变量的写操作不依赖于当前值
  2. 该变量没有包含在具有其他变量的不变式中