CAS是compare and swap的简称,将指定内存地址的内容与所给的某个值相比,如果相等,则将其内容替换为所给的另一个值,这是1个原子操作,不可被中断,在Java中,AQS用到了CAS,所以一系列的锁、AtomicInteger、synchronized等,都用到了CAS,CPU通过总线和内存进行数据传输,既然CAS要修改内存上的值,需要锁总线吗?这将会涉及到MESI缓存一致性协议。
MESI协议
现代CPU都有L1,L2和L3的3级缓存,L3一般是多个核心共享,L1和L2是核心单独访问的,既然有缓存,就会产生缓存不一致的情况,MESI协议就是用来保证缓存一致性的协议。
MESI协议的名称由来是指这一协议为缓存的每个数据单位(称为cache line,在Intel CPU上一般是64字节)维护两个状态位,使得每个数据单位可能处于M、E、S或I这四种状态之一。各种状态含义如下:
- M: 被修改的。处于这一状态的数据只在本CPU核心中有缓存,且其数据已被修改,没有更新到内存中。
- E: 独占的。处于这一状态的数据只在本CPU核心中有缓存,且其数据没有被修改,与内存一致。
- S: 共享的。处于这一状态的数据在多个CPU核心中有缓存。
- I: 无效的。本CPU核心中的这份缓存已经无效了。
如果当前核心中是I,那需要去主存中读取,如果不是I,都可以从缓存中读。这种读取的操作,都会被其它核心捕获这个信号,如果捕获到读取操作的核心的状态是M或者E,会把自己缓存中的数据,写回到主内存中去,并将自己的状态设置为S,这个时候,刚才的读操作才能继续执行。
- 只有M或者E的核心才可以修改数据,修改后状态修改为M。
- 如果CPU要修改数据时发现其缓存不处于E或M状态,则需要发出特殊的RFO指令(Read For Ownership),将其它CPU的缓存设为I状态。
- 如果要修改主内存中变量的值,一般来说是要锁住总线,不让其它核心修改。
- 如果1个线程,对1个变量的值进行多次修改,该核心的状态一直是M,就可以直接修改,不需要锁总线。
所以不一定要锁总线,就在于这个并发操作冲突的严重还是不严重,如果冲突很多,怎么多得锁住总线。在并发编程的时候,降低冲突也是一种手段。
写缓冲器和无效化队列
根据MESI协议,例如写了数据,要向总线发消息,等到其它核心回复之后,才能生效,这段等待的时间,这个核心就处于阻塞的状态,该怎么办呢?这时候就引入了写缓冲器和无效化队列,来优化这个阻塞。
- 状态为 E、M,直接写入,不向总线发送消息。
- 状态为 S,变量写入[写缓冲器],并发送Invalidate消息,继续下一个指令。
- 状态为 I,变量写入[写缓冲器],并发送Invalidate消息,继续下一个指令。
- 收到其他所有处理器回应的Read Response、Invalidate Acknowledge消息后,处理器才会将写缓冲器中对应的写操作写入相应的缓存行,这个时候,写操作才算真正完成。
- 读的时候先去[写缓冲器]中去读,如果没读到,再去缓存中读。
无效化队列的意思是说如果核心收到了其它核心的Invalidate消息,就先把消息消息存入到无效队列之后,就回复Invalidate Acknowledge,提高了效率,可以看到,队列的使用无处不在,你还在犹豫要不要使用队列吗?但是这样也带来了新的问题,每次读取缓存的时候,其它核心的写缓冲器中,可能有没有生效的值,那怎么办呢?
解决写缓冲器和无效化队列带来的可见性问题
解决办法就是内存屏障
存储屏障(Store Barrier): cpu 将写缓冲器排空,写入高速缓存 这叫冲刷,这样其他cpu 就会收到通知,其他cpu可以来拿新数据。
加载屏障(Load Barrier): cpu 根据无效化队列里面的信息,删除其高速缓存的无效数据(就是状态变为I)。
这2个屏障的成对使用,才能保证更新可见,本质上就是要保证,每次读数据之前,都已经看到了前面的所有的写,这个写肯定是已经生效了;
具体是: load load/store store/load store/store load
- LoadLoad屏障: 对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
- StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
- LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
- StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
Java中的volatile关键字其实有2个作用:
- 禁止重排序。
- 保证可见性,写操作之后的读肯定能看到最新值。
全文完。