預(yù)備知識
存儲器層次結(jié)構(gòu)
大學(xué)操作系統(tǒng)課程里講到了存儲器層次結(jié)構(gòu)的金字塔模型,金字塔從上到下代表更大的容量、更慢的存取速度、更低的成本。如下圖所示:
金字塔模型
為了解決CPU的高速與內(nèi)存磁盤低速之間的矛盾,處理器不直接和系統(tǒng)內(nèi)存進行通信,而是先將系統(tǒng)內(nèi)存的數(shù)據(jù)讀到內(nèi)部緩存(L1,L2,L3)后再進行操作,操作完成后在未來某個時間點會寫到內(nèi)存。
常用CPU術(shù)語
- 內(nèi)存屏障(memory barriers):一組處理器指令,用于實現(xiàn)對內(nèi)存操作的順序限制。
- 緩存行(cache line):CPU高速緩存中可以分配的最小存儲單位。緩存行大小根據(jù)架構(gòu)不同而不同,常見的有64Byte和32Byte,CPU填充緩存行時以緩存行為單位進行,每一次都讀取數(shù)據(jù)所在的整個緩存行,即使相鄰的數(shù)據(jù)沒有被用到也會被讀到CPU緩存中。
- 原子操作(atomic operations):不可中斷的一個或一系列操作。
- 緩存行填充(cache line fill):當(dāng)處理器識別到從內(nèi)存中讀取操作數(shù)是可緩存的,處理器讀取整個高速緩存行到適當(dāng)?shù)木彺?L1,L2,L3)
- 緩存命中(cache hit):如果進行高速緩存行填充操作的內(nèi)存位置仍然是下次處理器訪問的地址時,處理器從緩存中讀取操作數(shù),而不是內(nèi)存讀取。
- 寫命中(write hit):當(dāng)處理器將操作數(shù)寫回到一個內(nèi)存緩存的區(qū)域時,它首先會檢查這個緩存的內(nèi)存地址是否在緩存行中,如果存在一個有效的緩存行,則處理器將這個操作數(shù)寫回到緩存,而不是寫回到內(nèi)存,這個操作叫做寫命中。
- 寫缺失(write misses the cache):一個有效的緩存行被寫入到不存在的內(nèi)存區(qū)域。
- 寫通(write through):每次CPU修改Cache中的內(nèi)容,Cache立即更新主內(nèi)存的內(nèi)容。
- 寫回(write back):修改Cache的內(nèi)容后,Cache并不會立即更新內(nèi)存中的數(shù)據(jù),而是等到cache line因為某種原因需要從cache中移除時,cache才會更新主內(nèi)存中的內(nèi)容。
- 嗅探(snoop):每個處理器通過嗅探在地址總線上傳播的數(shù)據(jù)來檢查自己緩存值是不是過期了,當(dāng)處理器發(fā)現(xiàn)自己緩存行對應(yīng)的內(nèi)存地址被修改,就會將當(dāng)前的cache line設(shè)置成無效狀態(tài)。當(dāng)處理器對這個數(shù)據(jù)進行修改操作時,會重新從系統(tǒng)內(nèi)存把數(shù)據(jù)讀到Cache里。
緩存一致性
在多處理器下,每個CPU都有自己的私有緩存(可能共享L3緩存),當(dāng)一個CPU在進行寫內(nèi)存地址操作時,并且這個地址當(dāng)前處于共享狀態(tài),那么其它CPU的數(shù)據(jù)就是無效數(shù)據(jù)。緩存一致性就是為了保證多CPU之間的緩存是一致的。
IA-32處理器和Intel64處理器使用MESI控制協(xié)議處理緩存一致性。所謂MESI即是指CPU緩存的四種狀態(tài):
- M(Modified):某個處理器已經(jīng)修改緩存行,即是"dirty line",它的數(shù)據(jù)和主內(nèi)存中的數(shù)據(jù)不一致。該緩存行中的數(shù)據(jù)在未來某個時間點(允許其它CPU讀取相應(yīng)內(nèi)存之前)寫回(write back)主內(nèi)存。
- E(Exclusive):緩存行內(nèi)容和內(nèi)存中的一樣,數(shù)據(jù)只存在于本Cache中。
- S(Shared):緩存行內(nèi)容和內(nèi)存中的一樣,數(shù)據(jù)存在于很多Cache中。
- I(Invalid):緩存行數(shù)據(jù)無效,不能使用。
volatile應(yīng)用
Java語言規(guī)范第3版中對volatile的定義如下:Java編程語言允許線程訪問共享變量,為了確保共享變量能被準(zhǔn)確和一致地更新,線程應(yīng)該確保通過排他鎖單獨獲得這個變量。
volatile是輕量級的synchronized,它比synchronized使用和執(zhí)行成本更低,不會引起線程上下文的切換和調(diào)度。
volatile可以用來修飾字段,就是告知程序任何對該變量的訪問均需要從共享內(nèi)存中獲取,而對它的改變必須同步刷新回共享內(nèi)存,它能保證所有線程對變量訪問的可見性。
匯編指令分析
volatile關(guān)鍵字修飾的共享變量在進行寫操作,反匯編會發(fā)現(xiàn)多出一條匯編指令,如下所示:
// java代碼
volatile Singleton instance = new Singleton();
...
// 匯編后指令
0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: lock addl $0x0,(%esp);
Lock前綴的指令在多核處理器下會引發(fā)兩件事情:
1) 將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。在鎖操作時,總是在總線上聲言LOCK#信號,該信號確保在聲言自己期間內(nèi)CPU可以獨占任何共享內(nèi)存。在P6和目前處理器中,如果訪問的內(nèi)存區(qū)域已經(jīng)緩存在處理器內(nèi)部,則不會聲言LOCK#信號,相反它會鎖定這塊內(nèi)存區(qū)域的緩存并寫回到內(nèi)存,并使用緩存一致性機制來確保修改的原子性,這種操作被稱為"緩存鎖定"。
2) 這個寫回系統(tǒng)內(nèi)存的操作會使在其它CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效。由嗅探技術(shù)和緩存一致性可知。
追加字節(jié)優(yōu)化性能?
JDK7并發(fā)包的一個隊列集合類LinkedTransferQueue代碼如下:
private transient final PaddedAtomicReference<QNode> head;
private transient final PaddedAtomicReference<QNode> tail;
static final class PaddedAtomicReference<T> extends AtomicReference<T> {
Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe;
PaddedAtomicReference(T r) {
super(r);
}
}
public class AtomicReference<V> implements java.io.Serializable {
private volatile V value;
...
}
一個對象引用占4個字節(jié),追加了15個變量后共占64字節(jié)。目前主流處理器的
L1、L2或L3緩存的高速緩存行是64個字節(jié)寬,不支持部分填充緩存行。如果隊列的頭節(jié)點和尾節(jié)點都不足64字節(jié),處理器會將它們都讀到同一個高速緩存行中,當(dāng)一個處理器修改頭結(jié)點時會導(dǎo)致整個緩存行鎖定,在緩存一致性機制下,其它處理器不能訪問自己高速緩存中的尾節(jié)點,而入隊和出隊操作都需要不停修改頭節(jié)點和尾節(jié)點,所以會嚴(yán)重影響到出、入隊效率。
不應(yīng)使用追加到64字節(jié)的場景:
1) P6系列和奔騰處理器的L1和L2高速緩存行是32字節(jié)寬。
2) 追加字節(jié)的方式需要處理器讀取更多的字節(jié)到高速緩沖區(qū),本身會帶來一定的性能消耗。如果共享變量不被頻繁寫的話,緩存鎖定的幾率也是非常小的,沒有必要通過追加字節(jié)的方式來避免頭、尾節(jié)點相互鎖定。
這種追加字節(jié)方式在Java7下不生效了,因為Java7更加智慧,它會淘汰或重排列無用字段,需要其它追加字節(jié)的方式。