本文用于理解各种自旋锁对于缓存一致性风暴进行的优化
案例一: TAS锁引起的缓存一致性风暴
假设机器是64位的,CPU一次读取64bit,即8个字节的数据高速缓存L1的一个缓存块;有2个CPU,每个CPU都有一个L1高速缓存;4个线程,一次为T1,T2,T3,T4
;CPU1执行线程T1,CPU2执行线程T2,CPU1执行线程T3,CPU2执行线程T4;4个线程竞争锁的顺序是T1->T2->T3->T4,则
- 当线程1在CPU1上执行
while(state.getAndSet(true))
时,即state
记录的共享地址读取true
值到L1的一个缓存块,记录该缓存块d11的状态为exclusive; - 当线程2在CPU2执行
while(state.getAndSet(true))
时,CPU1检测到地址冲突,如果CPU1的L1里记录state
的缓存块的状态已经是shared,则不做操作;否则将CPU1的L1里记录state
的缓存块的状态修改为shared。CPU2从state
记录的共享地址读取true
值到L1的一个缓存块,记录该缓存块的状态为shared; - 当线程3在CPU1执行
while(state.getAndSet(true))
时,由于CPU1的L1里记录state
的缓存块的状态已经是shared,所以不做操作。线程4的情况类似,就省略; - 当线程1在CPU1上执行
state.set(false)
时,修改L1里记录该state
值的缓存块的状态为modified,并发起“修改各自高速缓存中记录共享数据的缓存块状态为invalid”的广播请求。CPU2收到该广播请求后,就将其上的L1里记录共享数据state
的缓存块状态为invalid; - 当线程2在CPU2执行
while(state.getAndSet(true))
时,发现其L1中记录该state
值的缓存块的状态为invalid,然后发起“从主内中读取共享数据”的广播请求。CPU1收到该广播请求后,将其上的L1里记录state
值的缓存块的数据false
刷新到主内存,并修改该缓存块的状态为shared。CPU2则从主内存中读取更新后的state
值false
到L1的一个缓存块,记录该缓存块的状态为shared; - 线程2释放锁,线程3获取锁;线程3释放锁,线程4获得锁;线程4释放锁都是在重复步骤4和步骤5
案例二:ArrayLock(有界队列锁)带来的缓存一致性风暴优化
情形一:假定boolean数据占据1个字节,一个CPU执行一个线程,此时的缓存一致风暴量跟TAS锁一样,就不做讨论了
情形二:假定boolean数据占据1个字节,2个CPU,4个线程:CPU1执行线程T1、T3,CPU2执行线程T2、T4,
- 当线程T1在CPU1上执行
while(!flags[0]){}
时,会一次读取8个字节的数据到L1的一个缓存块中,并标记该缓存块的状态为exclusive; - 当线程T2在CPU2上执行
while(!flags[1]){}
时,CPU1检测到地址冲突,如果L1中存储flags[1]值的缓存块的状态已经是shared,则不做操作;否则将L1存储flags[1]值的缓存块的状态修改为shared;CPU2会一次读取8个字节的数据到高速缓存L2的一个缓存块中,并标记该缓存块的状态为shared; - 当线程T3在CPU1上执行
while(!flags[2]){}
时,由于在步骤1中已经加载了flags[2]的数据,所以读命中,此时L1中记录flags[2]的状态为shared; - 当线程T4在CPU2上执行
while(!flags[3]){}
时,由于在步骤1中已经加载了flags[3]的数据,所以读命中,此时L2中记录flags[3]的状态为shared; - 当线程1在CPU1执行
flags[0]=false;flags[1]=true
时,则将flags[0]和flags[1]所在的缓存块的状态改为modified,并发起“修改各自记录共享数据的缓存块的状态为invalid”的广播请求。CPU2收到该广播请求后,就将各自高速缓存中记录共享数据的缓存块的状态为invalid; - 线程2在CPU2执行
while(!flags[1]){}
时,发现高速缓存L2中记录flags[1]的缓存块的状态为invalid,则就发起“从主内存中读取数据”的广播请求,此时CPU1就把L1记录的flag[1]所在的缓存块的数据刷新到主内存,并记录该缓存块的状态改为shared;CPU2从主内存中重新读取flags[1]的数据到L2的一个缓存行,并记录该缓存行的状态为shared - 当线程2在CPU2上执行
flags[1]=false;flags[2]=true
时,则将flags[1]和flags[2]所在的缓存块的状态改为modified,并发起“修改各自记录共享数据的缓存块的状态为invalid”的广播请求。CPU1收到该广播请求后,就将各自高速缓存中记录共享数据的缓存块的状态为invalid; - 线程3在CPU1执行
while(!flags[2]){}
时,发现高速缓存L2中记录flags[2]的缓存块的状态为invalid,则就发起“从主内存中读取数据”的广播请求,此时CPU2就把L2记录的flag[2]所在的缓存块的数据刷新到主内存,并记录该缓存块的状态改为shared;CPU1从主内存中重新读取flags[2]的数据到L1的一个缓存行,并记录该缓存行的状态为shared
参考资料
- 《The Art of Multi-Processor Programming》
- 聊聊高并发(三十四)Java内存模型那些事(二)理解CPU高速缓存的工作原理
http://blog.csdn.net/iter_zc/article/details/41979189 - 聊聊高并发(五)理解缓存一致性协议以及对并发编程的影响
http://blog.csdn.net/iter_zc/article/details/40342695