我们都知道,volatile保证了内存可见性和禁止指令重排,但是对于内存可见性这一条,我一直没有完全弄明白,今天咱们一起看一下,这个可见性,到底是如何可见,数据到底是如何可见的。
首先我们要达成一个共识:单核CPU由于同一时刻只会有一个线程执行,而每个线程执行的时候操作的都是同一个CPU的缓存,所以,单核CPU不存在可见性问题。
要了解清楚什么是内存可见性,我们需要先明确几个关键字的含义。
定义
cpu缓存
今天主流的CPU架构来说,现在的CPU主要采用三层缓存:
L1、L2缓存成为本地核心内缓存,即一个核一个。如果你的机器是4核,
那就是有4个L1+4个L2
L3缓存是所有核共享的。即不管你的CPU是几核,这个CPU中只有一个L3
L1缓存的大小是64K,即32K指令缓存+32K数据缓存。L2是256K,L3是2M。
MESI协议
MESI其实对应的是缓存中缓存行(CPU缓存存储数据的最小单位,大小为64B)的四种状态。
-
已修改Modified (M)
缓存行是脏的(dirty),与主存的值不同。如果别的CPU内核要读主存这块数据,该缓存行必须回写到主存,状态变为共享(S). -
独占Exclusive (E)
缓存行只在当前缓存中,但是干净的(clean)--缓存数据同于主存数据。当别的缓存读取它时,状态变为共享;当前写数据时,变为已修改状态。
3.共享Shared (S)
缓存行也存在于其它缓存中且是干净的。缓存行可以在任意时刻抛弃。
4.无效Invalid (I)
缓存行是无效的
举例
明确了定义,接下来我们用一个具体示例来描述一下MESI的可见性是如何起效的。
i=0; i=i+1;
执行这条语句的时候,在某个核上运行的某线程将 i 的值拷贝一个副本到此核所在的缓存中,当运算执行完成后,再回写到主存中去。如果是多线程环境下,每一个线程都会在所运行的核上的高速缓存区有一个对应的工作内存,也就是每一个线程都有自己的私有工作缓存区,用来存放运算需要的副本数据。那么,我们再来看这个 i+1 的问题,假设 i 的初始值为0,有两个线程同时执行这条语句,有以下几个步骤:
- 程序以及数据被加载到主内存
- 指令和数据被加载到CPU的高速缓存
- CPU执行指令,把结果写到高速缓存
- 高速缓存中的数据写回主内存
我们先讨论volatile是如何保证可见性的,假设此时线程1被cpu0执行,线程2被cpu1执行,cpu0和cpu1同时把i=0这个值从内存读取到了自己的缓存中,那么,根据MESI协议,此时cpu0和cpu1中的该缓存状态都变成S(共享),然后此时cpu0计算完成i+1=1,cpu1也计算完成i+1=1,cpu0首先将结果写回了主存,此时,cpu1缓存中的i值,将会变为I(无效)状态,此后如果cpu1再去读取i,就要重新从主存拿值,可是此时cpu1其实已经计算完成,所以cpu1也将结果1写回了主存。
从这个例子看,虽然保证了可见性,可是最终的计算结果还是错误的。这是因为volatile没有保证原子性,线程执行的这个i=i+1不是一个完整的不可分割的计算过程。
参考
Volatile:内存屏障原理应该没有比这篇文章讲的更清楚了
MESI协议
Java 开发, volatile 你必须了解一下