原始碼分析:升級版的讀寫鎖 StampedLock

Admol發表於2020-11-19

簡介

StampedLock 是JDK1.8 開始提供的一種鎖, 是對之前介紹的讀寫鎖 ReentrantReadWriteLock 的功能增強。StampedLock 有三種模式:Writing(讀)、Reading(寫)、Optimistic Reading(樂觀度),StampedLock 的功能不是基於AQS來實現的,而是完全自己內部實現的功能,不支援重入。在加鎖的時候會返回一個戳,解鎖的時候需要傳入,匹配完成解鎖操作。

官方使用示例

class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();

    void move(double deltaX, double deltaY) { // an exclusively locked method
        // 寫鎖-獨佔資源
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }

    double distanceFromOrigin() { // A read-only method
        // 只讀的方法,比較樂觀,認為讀的過程中不會有寫,所以這裡是樂觀度
        long stamp = sl.tryOptimisticRead();
        double currentX = x, currentY = y;
        if (!sl.validate(stamp)) { // 檢查樂觀讀鎖後是否有其他寫鎖發生
            // 獲取一個普通的讀鎖
            stamp = sl.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                // 釋放讀鎖
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

    void moveIfAtOrigin(double newX, double newY) { // upgrade
        // Could instead start with optimistic, not read mode
        long stamp = sl.readLock();
        try {
            while (x == 0.0 && y == 0.0) {
                // 普通讀鎖轉換成寫鎖,返回0為轉換失敗
                long ws = sl.tryConvertToWriteLock(stamp);
                if (ws != 0L) {
                    stamp = ws;
                    x = newX;
                    y = newY;
                    break;
                }  else {
                    sl.unlockRead(stamp);
                    stamp = sl.writeLock();
                }
            }
        } finally {
            sl.unlock(stamp);
        }
    }
}

官方demo中用到的 api 主要有獲取寫鎖(writeLock())、釋放寫鎖(unlockWrite(stamp))、獲取普通讀鎖(readLock())、釋放普通讀鎖(unlockRead(stamp))、獲取樂觀讀鎖(tryOptimisticRead())、檢測樂觀讀版本(validate(stamp))、普通讀鎖轉換成寫鎖(tryConvertToWriteLock(stamp))。下面分析原始碼的時候也主要根據這幾個方法來分析。

原始碼分析

主要內部類

  1. 等待節點:WNode
    用於維護 CLH 佇列的節點,原始碼如下:

    static final class WNode {
        volatile WNode prev;  // 前驅節點
        volatile WNode next;  // 後繼節點
        volatile WNode cowait;    // 連結的讀者列表
        volatile Thread thread;   // 執行緒
        volatile int status;      // 狀態 0, WAITING, or CANCELLED
        final int mode;           // 兩種模式:RMODE or WMODE
        WNode(int m, WNode p) { mode = m; prev = p; }
    }
    
  2. ReadWriteLockView:實現了ReadWriteLock介面,提供了讀寫鎖獲取介面

    final class ReadWriteLockView implements ReadWriteLock {
        public Lock readLock() { return asReadLock(); }
        public Lock writeLock() { return asWriteLock(); }
    }
    
  3. ReadLockView 和 WriteLockView:都實現了Lock介面,並實現了所有的方法

主要屬性

  1. CLH 佇列

    /** Head of CLH queue */
    private transient volatile WNode whead;
    /** Tail (last) of CLH queue */
    private transient volatile WNode wtail;
    
  2. 其他常量屬性

    /** CPU的核心數量,用來控制自旋的次數 */
    private static final int NCPU = Runtime.getRuntime().availableProcessors();
    /** 獲取鎖入隊前最大重試次數:CPU核心數大於1:64,否則0 */
    private static final int SPINS = (NCPU > 1) ? 1 << 6 : 0;
    /** 等待佇列的頭結點,獲取鎖最大重試次數:CPU核心數大於1:1024,否則0 */
    private static final int HEAD_SPINS = (NCPU > 1) ? 1 << 10 : 0;
    /** 再次阻塞前最大重試次數:CPU核心數大於1:65536,否則0  */
    private static final int MAX_HEAD_SPINS = (NCPU > 1) ? 1 << 16 : 0;
    /** The period for yielding when waiting for overflow spinlock */
    private static final int OVERFLOW_YIELD_RATE = 7; // must be power 2 - 1
    
    /** 用於讀取器計數的位數 */
    private static final int LG_READERS = 7;
    
    // 用來計算state值的常量
    private static final long RUNIT = 1L;  // 讀單位
    // 寫鎖的標識位 十進位制:128  二進位制位標示:1000 0000 
    private static final long WBIT  = 1L << LG_READERS;  
    // 讀狀態標識 admol 十進位制:127  二進位制: 0111 1111
    private static final long RBITS = WBIT - 1L;   
    // 讀鎖的最大標識  十進位制:126 二進位制 :0111 1110 
    private static final long RFULL = RBITS - 1L;  
    // 用來讀取讀寫狀態  十進位制:255 二進位制:1111 1111
    private static final long ABITS = RBITS | WBIT; 
    // ~255 ==  11111111111111111111111111111111111111111111111111111111 1000 0000
    // -128
    private static final long SBITS = ~RBITS; 
    
    // 同步狀態state的初始值 256  二進位制:0001 0000 0000
    private static final long ORIGIN = WBIT << 1;
    
    // 中斷
    private static final long INTERRUPTED = 1L;
    
    // 節點的狀態
    private static final int WAITING   = -1;
    private static final int CANCELLED =  1;
    
    // 節點的模式 
    private static final int RMODE = 0;
    private static final int WMODE = 1;
    
    /** 同步狀態 初始值 256 0001 0000 0000*/
    private transient volatile long state;
    /** 讀計數飽和時的額外讀取器計數 */
    private transient int readerOverflow;
    

    StampedLock 雖然沒有繼承AQS,但是屬性上很相似,都有一個CLH佇列,和一個同步狀態值state, StampedLock用8位來表示讀寫鎖狀態,前7位是用來標識讀鎖狀態的,第8位標識寫鎖佔用,如果讀鎖數量超過了126(0111 1110 ),超出的用readerOverflow來計數。

構造方法

public StampedLock() {
   // 初始值 256,  二進位制:0001 0000 0000
    state = ORIGIN;
}

獲取寫鎖:writeLock()

原始碼展示:

public long writeLock() {
    long s, next;  // bypass acquireWrite in fully unlocked case only
    return ((((s = state) & ABITS) == 0L &&  U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ? next : acquireWrite(false, 0L));
}

程式碼分析:

  1. 首先執行的是 ((s = state) & ABITS)== 0L,用來表示讀鎖和寫鎖是否可以被獲取
    解析:第一次時,state 初始值是256,ABITS是255,計算過程:0001 0000 0000 && 0000 1111 1111 ,結算結果為0。
  2. 執行CAS操作:U.compareAndSwapLong(this, STATE, s, next = s + WBIT))
    解析:STATE 是state欄位的記憶體地址相對於此物件的記憶體地址的偏移量,s 是期望值, next = s + WBIT 是更新值;s+WBIT 也就是 256 + 128,next計算結果為384,用二進位制位表示就是0001 1000 0000,也就是將第8位設定為1,就是獲得寫鎖。如果CAS 更新成功,返回next值,成功獲得寫鎖。
  3. 如果CAS執行失敗,則執行acquireWrite(false, 0L) ,進入等待佇列獲取鎖

acquireWrite 程式碼分析:

// interruptible 是否要檢查中斷
// deadline:0 一直等待獲取鎖
private long acquireWrite(boolean interruptible, long deadline) {
    // node:即將入隊排隊的節點
    // p:當前排隊節點入隊之前的尾節點
    WNode node = null, p;
    // 第一次自旋:排隊節點入佇列自旋
    for (int spins = -1;;) { // spin while enqueuing
        long m, s, ns;
        // 這個if 和外面一樣的,從程式碼執行到這期間所有沒有被釋放
        if ((m = (s = state) & ABITS) == 0L) {
            // CAS 再次嘗試獲取下寫鎖
            if (U.compareAndSwapLong(this, STATE, s, ns = s + WBIT))
                // 成功獲取寫鎖
                return ns;
        }  else if (spins < 0)  // 走到這,說明上面還是沒獲取到寫鎖,寫鎖被佔用了,m的值為128
            // 1. 確定自旋的次數 spins: 64 or 0 
            spins = (m == WBIT && wtail == whead) ? SPINS : 0;
        else if (spins > 0) {
            // 2.自旋的次數大於0,隨機減一次自旋次數,直到減到spins為0(by.精靈王 這裡實際是空轉,沒什麼特點的邏輯處理)
            if (LockSupport.nextSecondarySeed() >= 0)
                --spins;
        } else if ((p = wtail) == null) { // initialize queue
            // 3.自旋spins減到0後會立馬執行到這裡
            // p被賦值為尾節點  
            // 初始化佇列, WMODE:寫,null:前驅節點
            WNode hd = new WNode(WMODE, null);
            if (U.compareAndSwapObject(this, WHEAD, null, hd))
                // 初始化佇列時,尾節點等於頭節點
                wtail = hd;
        } else if (node == null)
            // 4.初始化佇列後,下一次自旋,構建當前排隊節點,並指定了其尾節點
            node = new WNode(WMODE, p);
        else if (node.prev != p) // 如果當前節點的前驅不是尾節點
            // 5.前驅節點設定為之前佇列的尾節點
            node.prev = p;
        else if (U.compareAndSwapObject(this, WTAIL, p, node)) {
            // 6. CAS 更新尾節點為當前排隊的節點 
            p.next = node;
            // 退出自旋
            break;
        }
    }
    // 第二次自旋
    for (int spins = -1;;) {
        WNode h, np, pp; int ps; 
        if ((h = whead) == p) { // 如果頭節點和之前的尾節點p是同一個, 說明馬上應該輪到node節點獲得鎖
            if (spins < 0)
                // ① 設定自旋次數  1024 or 0
                spins = HEAD_SPINS;
            else if (spins < MAX_HEAD_SPINS)
                // 第一次自旋1024次沒有獲取到鎖,這次自旋翻倍
                // 自旋次數*2  2048   繼續進入到下面的自旋
                spins <<= 1;
            for (int k = spins;;) { // spin at head
                // ② 第三次自旋,不斷嘗試獲得鎖(自旋1024或者2048次),直到成功獲得鎖 或者 break
                long s, ns;
                // ((s = state) & ABITS) == 0L  表示鎖沒有被佔用
                if (((s = state) & ABITS) == 0L) {
                    // CAS 修改state值
                    if (U.compareAndSwapLong(this, STATE, s,  ns = s + WBIT)) {
                        // CAS 修改成功獲得鎖,設定新的頭結點 
                        whead = node;
                        node.prev = null;
                        return ns;
                    }
                } else if (LockSupport.nextSecondarySeed() >= 0 && --k <= 0)
                    // 隨機立減自旋次數  自旋次數為0時跳出自旋迴圈
                    break;
            }
        } else if (h != null) { // help release stale waiters
            // 頭節點不為空
            // 進入情景:寫鎖被獲取,佇列中很多等待獲取讀鎖的執行緒,寫鎖釋放,讀鎖被喚醒後可能進入到這裡
            WNode c; Thread w;
            while ((c = h.cowait) != null) { // 自旋
                // 頭節點的 cowait不為空
                // h.cowait 修改成節點的下一個cowait
                if (U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) && (w = c.thread) != null)
                    // 喚醒 cowait 裡面的執行緒
                    U.unpark(w);
            }
        }
        if (whead == h) { // 如果頭結點沒有變化
            // P 是之前的尾節點
            if ((np = node.prev) != p) { 
                // != 之前的尾節點,也就是說當前節點的前驅節點不是尾節點時
                if (np != null)
                    // 儲存尾節點和當前節點的連線關係
                    (p = np).next = node;   // stale
            }
            else if ((ps = p.status) == 0)
                // ③ 上面第三次自旋break後會進入到這裡,修改尾節點狀態
                // 更新尾節點的狀態為WAITING:-1, 然後繼續回到第二次自旋的地方,重新開始自旋
                U.compareAndSwapInt(p, WSTATUS, 0, WAITING);
            else if (ps == CANCELLED) {
                // p節點狀態是取消,則刪除p節點
                if ((pp = p.prev) != null) {
                    node.prev = pp;
                    pp.next = node;
                }
            } else {
                // 超時時間
                long time; // 0 argument to park means no timeout
                if (deadline == 0L)
                    time = 0L;
                else if ((time = deadline - System.nanoTime()) <= 0L)
                    // 設定了超時時間 已經超時,取消當前node節點
                    return cancelWaiter(node, node, false);
                Thread wt = Thread.currentThread();
                // 為給定地址設定值,忽略修飾限定符的訪問限制,與此類似操作還有: putInt,putDouble,putLong,putChar等
                U.putObject(wt, PARKBLOCKER, this);
                // node節點指向當前執行緒
                node.thread = wt;
                // p.status < 0 的只有-1 也就是WAITING 
                if (p.status < 0 && (p != h || (state & ABITS) != 0L) && whead == h && node.prev == p)
                    // 阻塞當前執行緒
                    U.park(false, time);  // emulate LockSupport.park
                // 執行緒被喚醒後,清除節點的執行緒
                node.thread = null;
                U.putObject(wt, PARKBLOCKER, null);
                if (interruptible && Thread.interrupted()) // 要檢查中斷 && 執行緒有被中斷
                    // 取消當前node節點
                    return cancelWaiter(node, node, true);
            }
        }
    }
}

獲取寫鎖過程總結:

  1. 首先檢查鎖有沒有被佔用
    1. 沒有被佔用,嘗試 CAS 修改state值,CAS 修改成功則獲得鎖,返回新的 state 值。
    2. CAS 修改失敗的話進入下面自旋的邏輯
  2. 第一層自旋(節點入隊):
    1. 先檢查下鎖有沒有被佔用((m = (s = state) & ABITS) == 0L), CAS 嘗試一下獲取鎖,獲取失敗再繼續自旋
    2. 入隊之前會自旋64次(CPU核心數大於1),期間不做任何處理
    3. 初始化排隊佇列的隊頭隊尾節點,當前節點加入到隊尾,CAS 更新尾節點,更新成功則退出第一次自旋
  3. 開始第二層自旋(嘗試獲取鎖,阻塞執行緒),第二層自旋和第三層自旋巢狀執行的:
    1. 如果頭節點和之前的尾節點p還是是同一個(沒有其他獲取鎖的節點排隊已經入隊), 說明馬上應該輪到node節點獲得鎖(排隊的只有node節點)。
      1. 初始化第三層自旋次數(第一次1024,第二次2048),開啟第三層自旋
        1. 位運算檢查鎖是否有被釋放((s = state) & ABITS) == 0L),CAS 修改 state 值,修改成功,退出,返回新的state值
      • 這裡其實就是自旋和park執行緒之間效能的一個權衡,馬上就要獲得鎖了,是自旋還是阻塞執行緒繼續等,這裡選擇了先自旋1024次,如果沒有獲得鎖,繼續自旋2048次,如果還是沒獲得鎖,則退出第三層自旋,回到第二層自旋,準備阻塞當前執行緒。
    2. 如果排隊的頭結點不為空,檢查頭結點的cowait 連結串列,如果不為空,自旋 CAS 修改頭節點的cowait, 嘗試喚醒整個鏈的節點執行緒
    3. 第三層自旋完成後還是沒有獲取到鎖,阻塞當前執行緒,等待被喚醒,被喚醒後繼續第二層自旋獲取鎖,重複這個過程,直到獲取鎖成功推出。

釋放寫鎖:unlockWrite(stamp)

public void unlockWrite(long stamp) {
    WNode h;
    // state != stamp  檢查解鎖與加鎖的版本是否匹配
    // (stamp & WBIT) == 0L 為true的話說明鎖沒有被佔用
    if (state != stamp || (stamp & WBIT) == 0L)
        // 丟擲異常
        throw new IllegalMonitorStateException();
    // 釋放寫鎖,會增加state的版本
    // stamp += WBIT 等於二進位制(第一次加寫鎖和解鎖) 0001 1000 0000 + 0000 1000 0000 == 0010 0000 0000
    // 解鎖會把stamp 的二進位制第8位設定為0
    // 相當於重新賦值state值
    state = (stamp += WBIT) == 0L ? ORIGIN : stamp;
    if ((h = whead) != null && h.status != 0) // 頭結點不為 && 狀態不為初始狀態0(一般是WAITING -1),說明佇列中有排隊獲取鎖的執行緒
         // 喚醒頭節點的後繼節點
         release(h);
}
private void release(WNode h) {
    if (h != null) {
        // q節點:頭節點的有效後繼節點
        // w: 需要喚醒的執行緒
        WNode q; Thread w;
        // 將頭節點的狀態設定成0
        U.compareAndSwapInt(h, WSTATUS, WAITING, 0);
        if ((q = h.next) == null || q.status == CANCELLED) { // 如果頭節點的後繼為空 或者 是取消狀態
            // 就從排隊的隊尾找一個有效的節點
            for (WNode t = wtail; t != null && t != h; t = t.prev)
                if (t.status <= 0)
                    q = t;
        }
        // 找到了有效的節點,喚醒其執行緒
        if (q != null && (w = q.thread) != null)
            U.unpark(w);
    }
}

釋放寫鎖過程總結:

  1. 檢查鎖印章戳是否匹配,鎖是否有被佔用,檢查不通過丟擲異常
  2. 通過位運算stamp += WBIT計算新的state值,state 二進位制位的第8位會被設定成0就是寫鎖解鎖
  3. 檢測佇列中是否有排隊獲取鎖的執行緒
    1. 喚醒下一個等待獲取鎖的執行緒(unpark(thread)

獲取普通讀鎖:readLock()

public long readLock() {
    long s = state, next;  // bypass acquireRead on common uncontended case
    // 在沒有執行緒獲得鎖的情況下,s的初始值是256 
    // whead == wtail 為true:表示佇列為空
    // (s & ABITS) < RFULL: 已獲取讀鎖的數小於最大值126
    return ((whead == wtail && (s & ABITS) < RFULL &&  U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) 
					? next : acquireRead(false, 0L));
}

程式碼解析:

  1. 佇列為空 && 已獲取讀鎖次數小於126 && CAS 修改 state 值
    1. 條件完全成立,成功獲得鎖,返回新的state值
  2. 沒有成功,進入到acquireRead(false, 0L)方法排隊獲取鎖

acquireRead原始碼展示(程式碼超100行,需耐心觀看):

private long acquireRead(boolean interruptible, long deadline) {
        // p節點為尾節點  node為入隊節點
        WNode node = null, p;
        // 第一層大迴圈 第一次自旋,是不是和獲取寫鎖的很像?
        for (int spins = -1;;) {
            WNode h;
            if ((h = whead) == (p = wtail)) { // 首尾節點相等,說明佇列為空,有執行緒在排隊不會進入if
                // 前面沒獲取到鎖,佇列又為空,是不是應該馬上就是當前執行緒獲取鎖了?
                // 第二次自旋 自旋64次 目的是為了看馬上能不能獲取鎖(排隊佇列為空,沒執行緒排隊時,會在這裡自旋獲取鎖)
                for (long m, s, ns;;) {
                    // 這裡是個三目運算,程式碼太長,拆開來看
                    // (m = (s = state) & ABITS) < RFULL;和進入readLock()方法時的條件一樣,判斷讀鎖的數是否達到最大值,只有寫鎖被獲取,這裡就是false
                    // 我們假設前面寫鎖被獲取了,現在獲取讀鎖,m 就是128,大於RFULL 126
                    // 沒有達到最大值, CAS 修改狀態值 U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT)
                    // 超過最大值了,記得前面那個readerOverflow屬性不?在tryIncReaderOverflow這累加   
                    if ((m = (s = state) & ABITS) < RFULL ? U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) :
                        (m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L))
                        return ns;
                    else if (m >= WBIT) {// if條件成立,說明說明被佔用
                        // 
                        if (spins > 0) {
                            if (LockSupport.nextSecondarySeed() >= 0)
                                --spins; // 隨機減自旋次數
                        } else {
                            if (spins == 0) { // 自旋次數減到0了,還沒獲取到讀鎖
                                WNode nh = whead, np = wtail;
                                if ((nh == h && np == p) || (h = nh) != (p = np))
                                    break; // 退出自旋
                            } 
                            spins = SPINS; // 初始自旋次數 64次
                        }
                    }
                }
               // 上面到這裡都是在處理佇列為空,馬上要獲取到鎖的情況
            } 
						if (p == null) { // 尾節點為空,初始化排隊佇列,有執行緒在排隊時不會進入到這裡
                WNode hd = new WNode(WMODE, null);
                if (U.compareAndSwapObject(this, WHEAD, null, hd)) // CAS 設定頭節點
                    // 執行到這裡後,會回到第一次的自旋,再次進入到第二次自旋,這次 spins 為0,只會自旋一次
                    wtail = hd;
            } else if (node == null)
                // 初始化當前入隊排隊節點,有執行緒在排隊時,直接進入到這裡,然後繼續第一次自旋
                node = new WNode(RMODE, p); 
            else if (h == p || p.mode != RMODE) { // 佇列為空 或者 尾節點不是讀模式
                // 排隊節點入隊
                if (node.prev != p)
                    node.prev = p; // 設定排隊節點的前驅節點
                    // 繼續第一次自旋     
                else if (U.compareAndSwapObject(this, WTAIL, p, node)) {  // CAS 修改尾節點
                    p.next = node; // 老的尾節點的後繼節點為當前節點
                    // 進入到這裡會退出第一層自旋, 直接進入到下面第二層的大的自旋
                    break;
                }                
            } else if (!U.compareAndSwapObject(p, WCOWAIT, node.cowait = p.cowait, node))
                // 上面那個if分支進不去,只有條件:佇列不為空 and  尾節點是讀模式 為真
                // 進入到了這裡,說明CAS失敗
                // 這裡的CAS 就是把當前節點加入到尾節點的cowait棧裡面
                // 從這裡可以看出加入的順序是個棧結構,先把舊的尾節點的cowait賦值給node節點的cowait,然後再把node節點賦值給尾節點
                node.cowait = null;
            else { 
                // 進入到這,說明上面的if分支都沒有進去,尾節點不為空,當前節點不為空,佇列不為空,尾節點是讀模式,上面CAS修改成功
                // 總結一下進入到這裡的條件就是,有個執行緒獲得了寫鎖還沒釋放,佇列中有讀執行緒在排隊
                // 第三次自旋,有執行緒在排隊獲取鎖時,會進入到這裡自旋
                for (;;) {
                    WNode pp, c; Thread w;
                    if ((h = whead) != null && (c = h.cowait) != null &&
                        U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
                        (w = c.thread) != null) // help release
                        // 頭節點不為空,且其cowait節點不為空,喚醒整個cowait棧的執行緒
                        U.unpark(w);
                    if (h == (pp = p.prev) || h == p || pp == null) {
                        // 頭節點等於尾節點的前驅節點 或者頭節點等於尾節點 或者 尾節點的前驅節點為空
                        // 說明還是馬上輪到自己獲得鎖       
                        long m, s, ns;
                        do {
                            // 判斷是否可以使用CAS獲取讀鎖             
                            if ((m = (s = state) & ABITS) < RFULL ?  
                                U.compareAndSwapLong(this, STATE, s,  ns = s + RUNIT) :
                                (m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L))
                                return ns;
                        } while (m < WBIT); // m < WBIT時表示寫鎖沒有被佔用,一直嘗試獲取鎖
                    }
                    if (whead == h && p.prev == pp) { // 對頭沒有發生變化 ,隊尾也沒發生變化
                        long time;
                        if (pp == null || h == p || p.status > 0) { // 隊尾的前驅節點為空或者 頭節點等於尾節點 或者 老的尾節點被取消(>0的狀態只有1,取消)
                            node = null; // 拋棄當前節點,退出當前迴圈,回到第一層的自旋,重新構建節點
                            break;
                        }
                        if (deadline == 0L)
                            time = 0L;
                        else if ((time = deadline - System.nanoTime()) <= 0L) // 超時
                            return cancelWaiter(node, p, false); // 取消節點
                        Thread wt = Thread.currentThread();
                        U.putObject(wt, PARKBLOCKER, this);
                        node.thread = wt;
                        if ((h != pp || (state & ABITS) == WBIT) &&
                            whead == h && p.prev == pp)
                            U.park(false, time); // 阻塞當前執行緒
                        // 執行緒被喚醒,開始繼續自旋獲取鎖  
                        node.thread = null;
                        U.putObject(wt, PARKBLOCKER, null);
                        if (interruptible && Thread.interrupted())
                            return cancelWaiter(node, p, true);
                    }
                }
            }
        }
        // 第二層大的迴圈 
        for (int spins = -1;;) {
            WNode h, np, pp; int ps;
            if ((h = whead) == p) {  // 如果佇列為空,說明馬上輪到當前執行緒獲得鎖了
                // 這個大的if 裡面做的就是獲取鎖
                if (spins < 0)
                    // 初始化本次自旋獲取鎖的次數:1024次
                    spins = HEAD_SPINS;
                else if (spins < MAX_HEAD_SPINS)
                    // 上面1024次自旋沒有獲取到鎖,就自旋翻倍:2048次,繼續下面的自旋
                    spins <<= 1;
                // 開始自旋獲取鎖
                for (int k = spins;;) { // spin at head
                    long m, s, ns;
                    // 這個if條件 檢查了是否可以獲取鎖,如果可以就CAS獲取鎖
                    if ((m = (s = state) & ABITS) < RFULL ?
                        U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) :
                        (m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L)) {
                        // 進入到這個if裡面,說明就獲得了鎖
                        WNode c; Thread w;
                        // 這裡的node節點是還沒有繫結thread的
                        whead = node;
                        node.prev = null;
                        // 要喚醒當前node節點中的所有cowait節點執行緒
                        // 當前節點是在上面的第一層大的自旋入隊的,其他獲取讀鎖的節點都是掛在這個節點的cowait下的
                        while ((c = node.cowait) != null) {
                            if (U.compareAndSwapObject(node, WCOWAIT,
                                                       c, c.cowait) &&
                                (w = c.thread) != null)
                                U.unpark(w); // 喚醒執行緒
                        }
                        return ns;
                    }  else if (m >= WBIT &&  LockSupport.nextSecondarySeed() >= 0 && --k <= 0)
                        // 上面沒有獲取到鎖,自旋減次數,直到為0,退出自旋
                        break;
                }
            } else if (h != null) { // 佇列不為空,頭節點不為空
                WNode c; Thread w;
                while ((c = h.cowait) != null) {
                    if (U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
                        (w = c.thread) != null)
                        U.unpark(w);
                }
            }
            // 執行到這,說明還沒獲取到鎖
            if (whead == h) { // 頭節點沒變過
                if ((np = node.prev) != p) { // 檢查節點的連結關係
                    if (np != null)
                        (p = np).next = node;   // stale
                }
                else if ((ps = p.status) == 0)
                    // 檢查尾節點的狀態,為0則更新成-1,回到第二層大的迴圈開始處
                    U.compareAndSwapInt(p, WSTATUS, 0, WAITING);
                else if (ps == CANCELLED) { // p節點被取消,刪除這個節點
                    if ((pp = p.prev) != null) {
                        node.prev = pp;
                        pp.next = node;
                    }
                } else { // 上面2048次自旋後還是沒獲取到鎖,進入到最終阻塞執行緒的環節
                    long time;
                    if (deadline == 0L)
                        time = 0L;
                    else if ((time = deadline - System.nanoTime()) <= 0L)
                        return cancelWaiter(node, node, false); // 超時了就取消當前節點
                    Thread wt = Thread.currentThread(); // 當前執行緒
                    U.putObject(wt, PARKBLOCKER, this);
                    node.thread = wt;
                    if (p.status < 0 && (p != h || (state & ABITS) == WBIT) && whead == h && node.prev == p)
                        U.park(false, time); // 阻塞執行緒
                    // 執行緒被喚醒了,繼續執行 第二層大的自旋獲取鎖
                    node.thread = null;
                    U.putObject(wt, PARKBLOCKER, null);
                    if (interruptible && Thread.interrupted()) // 被喚醒了,發現要求中斷執行緒 並且執行緒被中斷了
                        return cancelWaiter(node, node, true); // 取消當前節點
                }
            }
        }
    }

獲取普通讀鎖總結:

  • 位運算檢查寫鎖是否被佔用,讀鎖是否超限制
    1. 滿足條件,直接CAS 修改state值,並返回新的state值
  • 開始第一層大的自旋,第一層大的自旋里麵包含了兩種情況不同的自旋:
    1. 第一種自旋情況:排隊的佇列為空,沒有其他執行緒在排隊等待鎖時
      • 這種情況說明,鎖雖然已經被佔用,但是馬上就應該是我得到鎖了,所以我先在這兒自旋(64次)等等你釋放鎖,免得阻塞我自己,之後喚醒還需要成本
      • 自旋沒有獲取到鎖,會退出第一層大的自旋,進入到第二層大的自旋
    2. 第二種自旋情況:寫鎖被佔用,排隊的佇列不為空,隊尾是讀模式時
      • 這種情況,會不斷嘗試獲取鎖,阻塞執行緒,等待被喚醒,一直在這個自旋里面,直到獲得鎖,或者超時中斷被取消,不會進入到第二層大的自旋
  • 第二層大的自旋
    • 這一層的自旋是對第一層裡面第一種自旋情況(馬上輪到我獲得鎖,但是前面持有鎖的執行緒就是不釋放)的補充,因為沒有執行緒在排隊,只要前面的執行緒釋放了鎖,馬上就可以獲得鎖了,所以這一層還是在自旋獲得鎖,只不過自旋次數有增加
      • 首先會嘗試自旋1024次獲得鎖,如果前面還沒釋放鎖,再自旋2048次
    • 如果2048次之後還是沒有等到前面的鎖釋放,就阻塞當前執行緒,等待被喚醒,直到獲得鎖,或者超時中斷被取消

cowait棧分析

下面程式碼是main執行緒先獲取寫鎖不釋放,之後T0,T1,T2,T3執行緒先後去獲取讀鎖,最後斷點觀察整個排隊佇列的情況

StampedLock sl = new StampedLock();
long stamp = sl.writeLock();
// 先讓T0執行緒去排隊到尾節點
TimeUnit.SECONDS.sleep(1);
new Thread(new Runnable(){
    @SneakyThrows
    @Override
    public void run(){
        long stamp = sl.readLock();
        System.out.println(stamp + "   x");
    }
},"T0").start();

// 之後T1執行緒來獲取讀
TimeUnit.SECONDS.sleep(3);
new Thread(new Runnable(){
    @SneakyThrows
    @Override
    public void run(){
        long stamp = sl.readLock();
        System.out.println(stamp + "   x");
    }
},"T1").start();
TimeUnit.SECONDS.sleep(3);
new Thread(new Runnable(){
    @SneakyThrows
    @Override
    public void run(){
        long stamp = sl.readLock();
        System.out.println(stamp + "   x");
    }
},"T2").start();
TimeUnit.SECONDS.sleep(3); // 在這裡先斷點,進入到這裡後,再到原始碼位置去斷點,就可以看到如下圖的情況了
new Thread(new Runnable(){
    @SneakyThrows
    @Override
    public void run(){
        long stamp = sl.readLock();
        System.out.println(stamp + "   x");
    }
},"T3").start();

執行程式碼後,斷點截圖:
Untitled

他們的關係可以用如下表示,橫向是連結串列,縱向是cowait棧。
微信圖片_20201119181133

釋放讀鎖:unlockWrite(stamp)

public void unlockRead(long stamp) {
    long s, m; WNode h;
    for (;;) { // 自旋
        if (((s = state) & SBITS) != (stamp & SBITS) || (stamp & ABITS) == 0L || (m = s & ABITS) == 0L || m == WBIT)
            // 檢查版本
            throw new IllegalMonitorStateException();
        if (m < RFULL) {  // 鎖標識小於讀鎖的最大標識
            if (U.compareAndSwapLong(this, STATE, s, s - RUNIT)) { // CAS 更新state值
                if (m == RUNIT && (h = whead) != null && h.status != 0) // 頭結點不為空
                    release(h); // 喚醒下一個節點
                break;
            }
        } else if (tryDecReaderOverflow(s) != 0L) 
            // 讀鎖個數飽和溢位,嘗試減少readerOverflow
            break;
    }
}
private void release(WNode h) {
    if (h != null) {
        WNode q; Thread w;
        U.compareAndSwapInt(h, WSTATUS, WAITING, 0);
        if ((q = h.next) == null || q.status == CANCELLED) {
            for (WNode t = wtail; t != null && t != h; t = t.prev)
                if (t.status <= 0)
                    q = t;
        }
        if (q != null && (w = q.thread) != null)
            U.unpark(w);
    }
}

釋放讀鎖的邏輯也比較簡單,和釋放寫鎖的邏輯很相識,喚醒下一個節點的release方法也完全一致

獲取樂觀讀鎖:tryOptimisticRead()

public long tryOptimisticRead() {
    long s;
    return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}

樂觀讀鎖的邏輯也比較簡單,就一個三目運算,((s = state) & WBIT) == 0L 就是看寫鎖是否有被佔用,寫鎖被佔用返回0,否則返回寫鎖沒被佔用的包含高位版本有效戳(也就是寫鎖的版本)。

檢測樂觀讀版本:validate(stamp)

public boolean validate(long stamp) {
    // 插入記憶體屏障,禁止load操作重排序。
    // 由於StampedLock提供的樂觀讀鎖不阻塞寫執行緒獲取讀鎖,當執行緒共享變數從主記憶體load到執行緒工作記憶體時,會存在資料不一致問題
    // 解決鎖狀態校驗運算髮生重排序導致鎖狀態校驗不準確的問題
	  U.loadFence(); 
	  return (stamp & SBITS) == (state & SBITS);
}

返回true:表示期間沒有寫鎖發生,讀鎖為所謂

返回false:表示期間有寫鎖發生

那這裡是怎麼計算的呢?

SBITS為-128,用二進位制表示是:1111 1111 1111 1000 0000

兩種情況:

  1. 假如先獲取樂觀鎖,再獲取讀鎖;
    樂觀鎖返回的stamp為256,二進位制位是 0001 0000 0000;
    獲取讀鎖之後state值是257,二進位制位是 0001 0000 0001;
    它們分別於與-128 進行與運算後都是0001 0000 0000,也就是十進位制256,返回true;
  2. 假如先獲取樂觀鎖,再獲取寫鎖;
    樂觀鎖返回的stamp為256,二進位制位是 0001 0000 0000;
    獲取寫鎖之後state值是384,二進位制位是 0001 1000 0000;
    它們分別與-128 進行與運算後,相當與 256 == 384,結果肯定返回false;

普通讀鎖轉換成寫鎖:tryConvertToWriteLock(stamp)

public long tryConvertToWriteLock(long stamp) {
    // m標識最新的鎖標識
    // a標識被轉換的鎖的鎖標識
    long a = stamp & ABITS, m, s, next;
    while (((s = state) & SBITS) == (stamp & SBITS)) { // 檢查鎖持有狀態
        if ((m = s & ABITS) == 0L) {
            if (a != 0L)
                break;
            if (U.compareAndSwapLong(this, STATE, s, next = s + WBIT))
                return next;
        } else if (m == WBIT) { // 寫鎖已經被佔用
            if (a != m)
                break;
            return stamp;   // 說明被轉換前就是寫鎖
        } else if (m == RUNIT && a != 0L) { // 被轉換前的是普通讀鎖,寫鎖沒被佔用
            if (U.compareAndSwapLong(this, STATE, s, next = s - RUNIT + WBIT))
                // s:是之前的鎖狀態
                // s - RUNIT:就是釋放讀鎖
                //  + WBIT :就是加寫鎖(進入之前寫鎖沒被佔用)
                return next; // 返回最新的鎖狀態
        } else
            break;  // 其他情況,全部返回0,轉換失敗
    }
    return 0L; // 返回0,標識轉換寫鎖失敗
}

普通讀鎖轉換成寫鎖過程總結:

  1. 如果轉換前是寫鎖,直接返回寫鎖
  2. 如果轉換前是讀鎖,轉換期間,寫鎖被佔用,返回0,轉換失敗
  3. 如果轉換前是讀鎖,寫鎖沒有被佔用,釋放讀鎖,加寫鎖,返回寫鎖,轉換成功
  4. 其他情況,全部返回0,轉換失敗

StampedLock 總結

  1. StampedLock 是一種支援樂觀讀鎖的高階版讀寫鎖
  2. StampedLock 沒有使用AQS 同步框架,而是完全自己實現的同步狀態state 和 CLH佇列維護演算法
  3. 同步狀態state的低7位標識讀鎖的數量,第8位標識寫鎖是否被佔用,高24位記錄寫鎖的版本,每次釋放寫鎖會版本位置會加1
  4. 寫鎖每次獲取state會加128,釋放也會加128,讀鎖是加減1
  5. StampedLock 的連續多個讀鎖執行緒,只有第一個是在佇列上,後面的讀執行緒都存在第一個執行緒的cowait棧結構上
  6. StampedLock 喚醒一個讀鎖執行緒後,讀鎖執行緒會喚醒所有在它cowait棧上的等待讀鎖執行緒
  7. StampedLock 用到了大量的自旋操作,適合持有鎖時間比較短的任務,持有鎖時間長的話等待的執行緒自旋後還是會阻塞自己。
  8. StampedLock 同一個執行緒先獲取讀鎖,再獲取寫鎖也會死鎖
  9. StampedLock 寫鎖不支援重入,讀鎖支援重入
  10. StampedLock 不支援條件鎖
  11. StampedLock 不支援公平鎖,上來有條件就 CAS 嘗試獲得鎖

StampedLock 與 ReentrantReadWriteLock的區別總結

使用功能上的區別:

  1. StampedLock 支援樂觀讀鎖,RRWL 沒有
  2. StampedLock 支援鎖轉換,tryConvertToXXXXX(stamp)
  3. StampedLock 寫鎖不支援重入,RRWL 支援重入
  4. StampedLock 不支援條件鎖,RRWL 支援條件鎖
  5. StampedLock 不支援公平鎖,RRWL 支援公平鎖

底層實現的區別:

  1. StampedLock 沒有使用同步框架AQS,RRWL 是基於AQS 來實現排隊、阻塞、喚醒等功能的
  2. StampedLock 獲取鎖時,會直接使用CAS嘗試獲得鎖(不公平,不看排隊),會根據CPU核心數來決定自旋次數等待獲取鎖
  3. StampedLock 的 CLH 佇列中連續的讀執行緒只有首個節點儲存在佇列中,後面的節點都儲存的首個節點的cowait棧中,即 1→5→4→3→2→1 這種順序。
  4. StampedLock 中同步狀態 state 被分成了三部分,第8位記錄的是寫鎖的狀態,低7位記錄讀鎖的次數,其他位記錄的是寫鎖的版本
  5. RRWL 中同步狀態 state 被分成兩部分,高16位記錄讀鎖次數,低16位記錄寫鎖次數
  6. StampedLock 喚醒一個讀鎖執行緒後,讀執行緒會喚醒所有在它cowait棧上的等待讀鎖執行緒

相關文章