Java并發(fā)機制底層實現(xiàn)(一)

預(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ù)語

  1. 內(nèi)存屏障(memory barriers):一組處理器指令,用于實現(xiàn)對內(nèi)存操作的順序限制。
  2. 緩存行(cache line):CPU高速緩存中可以分配的最小存儲單位。緩存行大小根據(jù)架構(gòu)不同而不同,常見的有64Byte和32Byte,CPU填充緩存行時以緩存行為單位進行,每一次都讀取數(shù)據(jù)所在的整個緩存行,即使相鄰的數(shù)據(jù)沒有被用到也會被讀到CPU緩存中。
  3. 原子操作(atomic operations):不可中斷的一個或一系列操作。
  4. 緩存行填充(cache line fill):當(dāng)處理器識別到從內(nèi)存中讀取操作數(shù)是可緩存的,處理器讀取整個高速緩存行到適當(dāng)?shù)木彺?L1,L2,L3)
  5. 緩存命中(cache hit):如果進行高速緩存行填充操作的內(nèi)存位置仍然是下次處理器訪問的地址時,處理器從緩存中讀取操作數(shù),而不是內(nèi)存讀取。
  6. 寫命中(write hit):當(dāng)處理器將操作數(shù)寫回到一個內(nèi)存緩存的區(qū)域時,它首先會檢查這個緩存的內(nèi)存地址是否在緩存行中,如果存在一個有效的緩存行,則處理器將這個操作數(shù)寫回到緩存,而不是寫回到內(nèi)存,這個操作叫做寫命中。
  7. 寫缺失(write misses the cache):一個有效的緩存行被寫入到不存在的內(nèi)存區(qū)域。
  8. 寫通(write through):每次CPU修改Cache中的內(nèi)容,Cache立即更新主內(nèi)存的內(nèi)容。
  9. 寫回(write back):修改Cache的內(nèi)容后,Cache并不會立即更新內(nèi)存中的數(shù)據(jù),而是等到cache line因為某種原因需要從cache中移除時,cache才會更新主內(nèi)存中的內(nèi)容。
  10. 嗅探(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、L2L3緩存的高速緩存行是64個字節(jié)寬,不支持部分填充緩存行。如果隊列的頭節(jié)點和尾節(jié)點都不足64字節(jié),處理器會將它們都讀到同一個高速緩存行中,當(dāng)一個處理器修改頭結(jié)點時會導(dǎo)致整個緩存行鎖定,在緩存一致性機制下,其它處理器不能訪問自己高速緩存中的尾節(jié)點,而入隊和出隊操作都需要不停修改頭節(jié)點和尾節(jié)點,所以會嚴(yán)重影響到出、入隊效率。

不應(yīng)使用追加到64字節(jié)的場景
1) P6系列和奔騰處理器的L1L2高速緩存行是32字節(jié)寬。
2) 追加字節(jié)的方式需要處理器讀取更多的字節(jié)到高速緩沖區(qū),本身會帶來一定的性能消耗。如果共享變量不被頻繁寫的話,緩存鎖定的幾率也是非常小的,沒有必要通過追加字節(jié)的方式來避免頭、尾節(jié)點相互鎖定。

這種追加字節(jié)方式在Java7下不生效了,因為Java7更加智慧,它會淘汰或重排列無用字段,需要其它追加字節(jié)的方式。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容