JUC之JDK自帶鎖StampedLock

一、初見

StampedLock是JDK 1.8的一把新鎖,同樣出自Doug Lee之手。這貨高級了,出身顯赫、自帶光環,有着光輝的使命。她是一把不一樣的鎖,前面我們所整理過的兩把鎖(ReentantLock&ReentrantReadWriteLock)都是基於AQS框架實現,同時又都具有可重入性(當然可重入性不是由AQS框架帶來的)。然而她卻與衆不同,她是讀寫鎖,她是把樂觀鎖,她是基本於時間戳實現。她優化了ReentrantReadWriteLock提供更高更高的OPS,她意在替代ReentrantReadWriteLock提供高效易用的讀寫鎖。

A capability-based lock with three modes for controlling read/write access. The state of a StampedLock consists of a version and mode. Lock acquisition methods return a stamp that represents and controls access with respect to a lock state; “try” versions of these methods may instead return the special value zero to represent failure to acquire access. Lock release and conversion methods require stamps as arguments, and fail if they do not match the state of the lock.

二、走進不一樣

然而她沒有基於AQS框架實現,而是自己實現了CLH隊列;她不用Integer表示狀態了,改用Long表示狀態。她就是如此出衆,讓我們走進StampedLock的源碼,窺探她美貌與才華。

首先我自己並沒有真正用過把鎖,不過接下來所有也不是蝦扯蛋,據說我曾經看過的一些資料和StampedLock源碼來梳理。我儘管可能多的把我看過的一些有價值的資料羅列出來。

註釋裏提到它是通過提供一個stamp來控制訪問權限,當然對於這個我的理解不夠,需要理解清楚的你可以分享。正也因爲它的這個特性使得它變得比較脆弱,在極端條件容易拋出異常。
StampedLock相比於ReentrantReadWriteLock多一個tryOptimisticRead()方式提供樂觀讀鎖,實現一種只在讀前做讀鎖權限測試,即是不用讀操作過程中加鎖。對這個我們可以看看她的註釋裏的示例,這能讓大家更加清晰的理解。

2.1 她自己重新實現了CLH隊列

正因爲她重新實現了CLH隊列,所以她的源碼非常複雜,也就沒有ReentrantReadWriteLock那麼清晰了。當然她倆要是都差不多,那麼也就沒有要必要如此大動干戈了,重新寫出一個類來。

先看兩個術語:
1. 悲觀鎖
悲觀鎖,它很悲觀,有點被害妄想症。時時刻刻認爲總人會跟它同時操作造成髒讀,所以它很沒有安全感,所以它每個操作都會加鎖來保證同步。
2. 樂觀鎖
樂觀鎖,它又非常樂觀,它總覺得它非常幸運。只要是他去數據就不會有人來修改,所以它並不在讀數據時加鎖。

按國際慣例此處應該是先來看,不過現在還不是看代碼的時候。要是把源碼貼出來了,目測您都不想再繼續看下去了,要是不貼源碼有些地方講起來感覺空落落的。

2.2 讓我困惑的State

先看一下state,前面提過她把state的精度從Integer升級到了Long。恕在下愚鈍,真沒看出來她改用Long的用意。7bit的長度表示讀狀態,第8個bit用來區分讀寫鎖。如此說來,那她只允許128線程同時獲得讀鎖嗎?答案自然不是這樣。StampedLock把超出127後的部分,即溢出部分放到其它位置了。StampedLock弄出另一個還記錄溢出數,readerOverflow,用來記錄溢出部分的數量。

在StampedLock的註釋中提到,StampedLock的state由Version和Model兩部分組成
對於左邊第一位也就算是Model位,後7位當成Version位。當Model位爲1時,表示寫模式;Version位記錄當前讀者數。

實在沒看出StampedLock用Long的原由,希望知道的你可以分享。

2.3 重新來看看CLH隊列

接下來來看看CLH部分的內容。
從CLH的部分看最明顯差異是名字改成WNode,更強調wait;其次多一個讀者獲取讀鎖的等待鏈表;然後加了兩個屬性。

static final class WNode {
    volatile WNode prev;
    volatile WNode next;
    volatile WNode cowait;    // list of linked readers
    volatile Thread thread;   // non-null while possibly parked
    volatile int status;      // 0, WAITING, or CANCELLED
    final int mode;           // RMODE or WMODE
    WNode(int m, WNode p) { mode = m; prev = p; }
}

我們已然知道StampedLock把CLH隊列與Lock業務邏輯參合在一起實現,其實是很難單獨把CLH隊列拿出來看。更準確的說,就是基本都是CLH隊列的實現,它佔很大的篇幅。邏輯上也比較複雜,代碼上又一味的追求簡潔,使得她的源碼讀得起來就沒那麼自然和順暢了。下面舉兩個過度追求簡潔例子。

public long readLock() {
    long s = state, next;  // bypass acquireRead on common uncontended case
    return (
        (whead == wtail && (s & ABITS) < RFULL && 
        U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?
        next : acquireRead(false, 0L));
}

// 這麼看其實還是很難看明白的,但稍微變換一下就易懂很多了,這就是代碼的藝術。
public long readLock() {
    long next, s=state;
    if(whead == wtail && (s & ABITS) < RFULL) {
        if(U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) 
            return next;
    }
    return acquireRead(false, 0L);
}

-----------------------------------------------------------------------------
// acquireRead() 源碼,#1144
if ((m = (s = state) & ABITS) < RFULL ?
                        U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) :
                        (m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L))
                        return ns;
// 修改後
boolean v;
if ((m = (s = state) & ABITS) < RFULL) 
    v = U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT);
else 
    v = (m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L);
if(v) return ns;

我們知道不管是acquireRead()或者acquireWrite()其實流程上都差不太多,只是acquireRead()相對複雜一點而已。它們都帶有兩個for循環,它們即是流程控制也是自旋鎖的實現方式。

2.3.1 第一個for循環

而且兩個方法實現的功能都非常相似,它們的要點是
1. 初始化隊列
2. 新節點入隊
3. 取得關鍵節點

2.3.2 第二個for循環

  1. 校驗新節點的狀態

我個人認爲StampedLock比較難讀有兩個原因,第一個是前面提的過於簡潔;第二個是流程控制。由於StampedLock是自旋鎖,因而它是自身需要引入一些循環來支持這個特徵。這一塊可以不看的。

對於流程控制的話,即是採用了大量的if-else if。其實單純只是if-else if也不能說它難讀,如果再上面加個循環的話,那就變化相當複雜了。

先來看看acquireWrite()的第一個for循環。有請源碼:

// StampedLock#acquireLock
WNode node = null, p;
for (int spins = -1;;) { // spin while enqueuing
    long m, s, ns;
    if ((m = (s = state) & ABITS) == 0L) {
        if (U.compareAndSwapLong(this, STATE, s, ns = s + WBIT))
            return ns;
    }
    else if (spins < 0)
        spins = (m == WBIT && wtail == whead) ? SPINS : 0;
    else if (spins > 0) {
        if (LockSupport.nextSecondarySeed() >= 0)
            --spins;
    }
    else if ((p = wtail) == null) { // initialize queue
        WNode hd = new WNode(WMODE, null);
        if (U.compareAndSwapObject(this, WHEAD, null, hd))
            wtail = hd;
    }
    else if (node == null)
        node = new WNode(WMODE, p);
    else if (node.prev != p)
        node.prev = p;
    else if (U.compareAndSwapObject(this, WTAIL, p, node)) {
        p.next = node;
        break;
    }
}

想看這個流程其實需要一些假設條件,我們先假設
1. 剛剛創建這個鎖
2. 已經有一個以上的讀者獲取讀鎖

下面CLH隊列是新節點進入CLH隊列的流程,準確的說前3步是初始隊列,第4步是入隊。
CLH流程|0X300
CLH流程|center|0X300

對於acquireRead()方面的內容不再繼續看,因爲它比較複雜。哈哈哈

三、再見

ReentrantReadWriteLock很容易出現寫飢餓,在讀多寫少的時候。StampedLock的出現最主要目標就是優化這個場景,在寫少的時候情況採用樂觀鎖的方式來解決寫飢餓的問題。即是以”一種樂觀的心態“來讀數據,減少鎖資源的佔用。

在ReentrantReadWriteLock只允許用戶從鎖降級,即從寫降成讀,但是讀不能變成寫。
但是呢,但由於StampedLock也完全不支持,即不能升級,也不能降級。因爲它不是遞歸鎖,不能在寫鎖裏面去調用帶讀鎖的方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章