从硬件层面理解volatile(java)
目录
一、volatile的作用
-
可见性:当程序执行到volatile变量的读或写时,在其前面的操作肯定全部已经执行完毕,且结果已经对后面的操作可见;在其后面的操作肯定还没有执行
-
禁止指令重排:在CPU、编译器进行指令优化时,不能把volatile变量后面的语句放到其前面执行,也不能把volatile变量前面的语句放到其后面执行
二、为什么会有可见性问题
可见性问题,要从cpu的缓存一致性(cache coherence)开始说起
1、缓存行
在讲缓存一致性之前,我们先来说一下缓存行的概念
缓存是分段(line)的,一个段对应一块存储空间,称为缓存行,它是CPU缓存中可分配的最小存储单元,大小为32、64、128字节不等,与CPU的架构有关,通常为64字节。当CPU看到一条读取内存的指令时,它会把内存地址传递给一级数据缓存,一级数据缓存会检查它是否有这个内存地址对应的缓存段,如果没有就把整个缓存段从内存(或高一级的缓存)中加载进来。
2、cpu的缓存一致性
cpu的缓存一致性(cache coherence)是一种保证存储在多个缓存中的共享资源数据相同的机制。缓存不一致,是指相同数据在不同的缓存中呈现出不同的表现。
缓存不一致的问题,在多核CPU的系统中,比较容易出现。假设主存有一个x,值为5。核0和核3都从主存中加载x到缓存。此时核0更改x的值为8。此时核3的缓存中的x的值还是5,数据出现了不一致。
3、如何保证cpu的缓存一致性
缓存一致性的协议有多种,嗅探(snooping)协议比较常用,它的基本思想是:所有的内存传输都发生在一条共享的总线上,所有的处理器都能看到这条总线。缓存本身是独立的,但是内存是共享资源,所有的内存访问都要经过仲裁(同一个指令周期中,只有一个CPU缓存可以读写内存)。
CPU缓存不仅仅在做内存传输时才和总线打交道,而是不停在嗅探总线上发生的数据交换,跟踪其他缓存在做什么。当一个缓存行代表它的处理器去读写内存时,其他处理器都会得到通知,它们以此来使自己的缓存保持同步。只要某个处理器一写内存,其他处理器马上知道这块内存在它们的缓存中已失效。
3.1 MESI协议
MESI协议是当前最主流的缓存一致性协议,在MESI协议中,每个缓存行有4个状态,可用2个bit表示,它们分别是:
状态 | 描述 |
---|---|
M(Modified) | 这行数据有效,数据被修改了,和内存中的数据不一致,数据只存在本cache中 |
E(Exclusive) | 这行数据有效,数据和内存中的数据一致,数据只存在于本cache中 |
S(Shared) | 这行数据有效,数据和内存中的数据一致,数据存在于多个cache中 |
I(Invalid) | 这行数据无效 |
只有当缓存行处于E或M状态,处理器才能去写它,也就是说只有在这两种状态下,处理器是独占这个缓存行的。
当处理器想要写某个缓存行时,如果它没有独占权,必须发送一条“我要独占权”的请求给总线,这会通知其他处理器把它们拥有的同一缓存行的拷贝失效。只有在获得独占权之后,处理器才能开始修改数据。并且此时这个处理器知道,这个缓存行只有一份拷贝,在我自己的缓存里,所以不会有冲突
反之,如果其他处理器想要读取这个缓存行(马上就能知道,因为一直在嗅探总线),独占或者修改状态的缓存行必须先回到共享状态。如果是已修改的缓存行,那么还要先把内容写回到内存中。
看懂了下图,就看懂了MESI协议
4、为什么还有可见性问题
既然已经有了MESI协议,那为什么未用volatile修饰的变量,在不同的线程中,还是有可见性问题呢?
-
MESI协议只是保证了CPU的缓存一致性,volatile是java语言层面给出的保证。它们之间还差着java编译器、java虚拟机、JIT、操作系统、CPU核心
-
为了优化MESI协议的性能,cpu引入了store buffer、invalidate queue,它们也会导致可见性问题
-
有些CPU并没有支持MESI协议
5、store buffer
5.1 为什么引入store buffer
如下图所示,假设cpu0、cpu1的缓存中都有x(值为5),cpu0想要执行x=8。那么cpu0发送Invalidate消息给cpu1,cpu1发送将自己包含x的缓存行置为invalid状态,然后发送Acknowledge给cpu0,然后cpu0才能修改自己缓存中x的值为8,并将缓存行的状态置为modify。从发送Invalidate到接收到Acknowledge消息的这段时间,cpu0在等待,白白浪费了时间。
为了解决这个问题,在cpu和它的缓存之间,加上了store buffer。cpu0先将8(x=8)放入到store buffer,然后发出Invalidate消息,再等待Acknowledge消息返回的期间,cpu0可以继续执行下一条指令。当cpu0接收到Acknowledge消息时,可以将8从store buffer取出,刷入到自己的缓存中。
假设cpu0 x=8之后的指令是y=x+1,它将8放入store buffer之后,执行y=x+1,此时的x是从store buffer取(值为8),还是从缓存中取(值为5)?store buffer有值的话,当然先从store buffer中取,这叫做store forwarding。
5.2 store buffer带来的问题
store buffer还会带来乱序的问题
假设a,b的初始值为0,a在cpu1的缓存中,b在cpu0的缓存中
cpu0执行的伪代码如下