【多线程与高并发原理篇:2_缓存一致性的解决方案】
1. 概述
上一篇抛出了一个缓存不一致问题,即多线程在多cpu执行过程中,各cpu高速缓存之间会出现数据不一致,或者cpu高速缓存与主内存数据不一致。从计算机的发展历史看,解决缓存数据不一致,先后出现了两种方案,一种是总线加锁
,该方案现在基本不用了,主要原因是性能差。第二种是目前主流的缓存一致性协议
,也就是较为著名的MESI协议。
2. 总线加锁
cpu从主内存取数据到自己的高速缓存,或者将自己对应高速缓存数据同步刷新到主内存,都要经过总线。如下如所示:
如果线程2对flg=1要进行自增操作,首先cpu2要将主内存的数据flg=1读到自己的高速缓存,此时总线会加锁,然后再执行cpu2指令,将flg自增,并修改为flg=2,随后再写回高速缓存,最后同步刷新到主内存,这一过程结束后,总线才释放锁。
在总线加锁的过程中,cpu1就无法从主内存读取数据,也无法将已修改完的数据从高速缓存同步刷新到主内存,直到cpu2对应的线程完成加锁与释放锁的过程结后才能进行。
这个总线加锁机制,性能差,一旦多个线程对某个共享变量进行操作,就会因总线加锁导致线程串行化
的问题,多个cpu多线程并发运行的时候,效率低。
3. 缓存一致性协议MESI
缓存一致性协议解决了上面总线加锁性能差、效率低的问题。
该方案采用了对高速缓存中的数据基本单位缓存行(cache line)加锁
,而非通过总线加锁,在实现过程中,JVM会向处理器发送一条Lock前缀指令
,保证锁的范围限定在各cpu所在的高速缓存中,完成数据修改后,强制将数据刷新到主内存,这一过程中会给总线发送数据修改消息,其他cpu通过总线嗅探机制
,嗅探到自己高速缓存中的数据已修改,会失效自己当前的高速缓存数据,重新将主内存的最新数据加载到自己缓存,以保证主内存与各高速缓存中的数据一致性。
下面通过图例来说明:
线程1
读数据:
1-1
: 线程1对主内存数据进行读操作,将变量flg=1读到高速缓存,供cpu计算使用;
1-2
: cpu向总线发送嗅探消息,一旦有其他高速缓存向总线发送修改缓存的消息,就执行1-3
步骤;
1-3
: 失效自己高速缓存数据flg=1,将主内存中的最新数据重新读入自己的高速缓存。
线程2
写数据:
2-1
: 线程2对主内存数据进行自增操作,先读取到高速缓存,再通过cpu指令完成自增操作后,写回主高速缓存,该步骤会对自己的高速缓存中的最小单位缓存行加锁
;
2-2
: 最后将flg=2刷回主内存,数据经过总线时,会给总线发送数据修改消息;
上述过程中,如果线程1再次读高速缓存的数据,会通过嗅探消息发现数据已更改,并确认得知数据也已经强制刷新到主内存中,做1-3
步骤,失效自己的原先的flg=1,强制从主内存重新读取数据到cpu1的高速缓存中。
MESI协议在不同硬件底层有不同的实现方式,但可以在效果上达到两个目的:
(1)flush机制:对高速缓存的数据修改强制刷新到主内存;
(2)refresh机制:其他cpu如果嗅探到自己高速缓存的变量副本已被修改,失效自己的高速缓存,从主内存获取最新数据到高速缓存。
对于MESI协议的具体实现,里面还涉及到不少细节,将在后续的篇幅中慢慢展开。