ReentrantLock源碼解讀

基礎鋪墊

node包裝的狀態:

  • SIGNAL(-1) :線程的後繼線程正/已被阻塞,當該線程release或cancel時要重新這個後繼線程(unpark)
  • CANCELLED(1):因爲超時或中斷,該線程已經被取消
  • CONDITION(-2):表明該線程被處於條件隊列,就是因爲調用了Condition.await而被阻塞
  • PROPAGATE(-3):傳播共享鎖
  • 0:0代表無狀態

AQS的屬性結構

// ---------------需要注意的是這個head和tail是一個雙向鏈表--------------------------

// 頭結點,你直接把它當做 當前持有鎖的線程 可能是最好理解的
private transient volatile Node head;
// 阻塞的尾節點,每個新的節點進來,都插入到最後,也就形成了一個隱視的鏈表
private transient volatile Node tail;
// 這個是最重要的,不過也是最簡單的,代表當前鎖的狀態,0代表沒有被佔用,大於0代表有線程持有當前鎖
// 之所以說大於0,而不是等於1,是因爲鎖可以重入嘛,每次重入都加上1
private volatile int state;
// 代表當前持有獨佔鎖的線程,舉個最重要的使用例子,因爲鎖可以重入
// reentrantLock.lock()可以嵌套調用多次,所以每次用這個來判斷當前線程是否已經擁有了鎖
// if (currentThread == getExclusiveOwnerThread()) {state++}
private transient Thread exclusiveOwnerThread; //繼承自AbstractOwnableSynchronizer

Node的結構

static final class Node {
    /** Marker to indicate a node is waiting in shared mode */
    // 標識節點當前在共享模式下
    static final Node SHARED = new Node();
    /** Marker to indicate a node is waiting in exclusive mode */
    // 標識節點當前在獨佔模式下
    static final Node EXCLUSIVE = null;
  
    // ======== 下面的幾個int常量是給waitStatus用的 ===========
    /** waitStatus value to indicate thread has cancelled */
    // 代表此線程取消了爭搶這個鎖
    static final int CANCELLED =  1;
    /** waitStatus value to indicate successor's thread needs unparking */
    // 官方的描述是,其表示當前node的後繼節點對應的線程需要被喚醒
    static final int SIGNAL    = -1;
    /** waitStatus value to indicate thread is waiting on condition */
    // 表示線程處於等待的條件下的值,與下面的waitStatus對應,這在Lock中的condition中會使用
    static final int CONDITION = -2;
    /**
     * waitStatus value to indicate the next acquireShared should
     * unconditionally propagate
     */
    static final int PROPAGATE = -3;
    // =====================================================
  
    // 取值爲上面的1、-1、-2、-3,或者0(以後會講到)
    // 這麼理解,暫時只需要知道如果這個值 大於0 代表此線程取消了等待,
    // 也許就是說半天搶不到鎖,不搶了,ReentrantLock是可以指定timeouot的。。。
    volatile int waitStatus;
    // 前驅節點的引用
    volatile Node prev;
    // 後繼節點的引用
    volatile Node next;
    // 這個就是線程本尊
    volatile Thread thread;
}

這裏需要搞清楚的一個概念:

  1. headtail分別代表的是當前鏈表的第一個最後一個
  2. Node中的prevnext代表的是鏈表內的前繼節點後繼節點

lock 方法調用過程:

        // step : 1
        final void lock() {
            // 如果state狀態爲0的話,就爲他設置初始狀態
            if (compareAndSetState(0, 1))
                // 綁定當前線程,表示爲當前線程的獨佔鎖
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        
        // step : 2
        public final void acquire(int arg) {
        // 2.1 tryAcquire嘗試判斷是否鎖爲搶佔或者是否是重入鎖
        // 2.2 addWaiter方法負責把當前無法獲得鎖的線程包裝爲一個Node添加到隊尾
        // 2.3 acquireQueued
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
        }
        
        
        ////////////////////2.1///////////////////
        // step : 2.1
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
        
        // step : 2.2 
        final boolean nonfairTryAcquire(int acquires) {
            // 獲取當前線程
            final Thread current = Thread.currentThread();
            // 獲取當前鎖的狀態
            // 0代表沒有被佔用,大於0代表有線程持有當前鎖
            int c = getState();
            // 如果當前鎖沒有被佔用的時候,不存在競爭的時候
            if (c == 0) {
                //通過cas將初始值0設置爲1
                //如果CAS設置成功,則可以預計其他任何線程調用CAS都不會再成功,也就認爲當前線程得到了該鎖,也作爲Running線程,
                //很顯然這個Running線程並未進入等待隊列。
                // 如果搶佔成功....最終會返回fasle
                if (compareAndSetState(0, acquires)) {
                    //綁定當前線程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 如果已經被佔用了,先判斷是否是當前線程搶佔的
            // 換句話說就是判斷是否是重入鎖
            else if (current == getExclusiveOwnerThread()) {
                // 如果是重入鎖,或者是當前線程搶佔的
                // 則將state的值+1 , 表示重入次數
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                // 設置重入狀態
                setState(nextc);
                return true;
            }
            return false;
        } 
        
        
        /////////////////////////2.2//////////////////////
        // addWaiter方法負責把當前無法獲得鎖的線程包裝爲一個Node添加到隊尾
        private Node addWaiter(Node mode) {
        // 爲當前線程構建一個新的鏈表
        // 其中參數mode是獨佔鎖還是共享鎖,默認爲null,獨佔鎖
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
    
        Node pred = tail;
        // 如果當前鏈表末尾不爲空
        if (pred != null) {
            // 則將當前獨佔所的節點上級設置爲上一個
            node.prev = pred;
            // 通過CAS將tail節點設置爲node
            // 通俗一點講就是更新pred的節點,也就是尾節點
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 2.2.1
        enq(node);
        return node;
    }
    // 2.2.1 
    
    /** 
    該方法就是循環調用CAS,即使有高併發的場景,無限循環將會最終成功把當前線程追加到隊尾(或設置隊頭)。總而言之,addWaiter的目的就是通過CAS把當前線程追加到隊尾,並返回包裝後的Node實例。
    把線程要包裝爲Node對象的主要原因,除了用Node構造供虛擬隊列外,還用Node包裝了各種線程狀態,這些狀態被精心設計爲一些數字值:
        SIGNAL(-1) :線程的後繼線程正/已被阻塞,當該線程release或cancel時要重新這個後繼線程(unpark)
        CANCELLED(1):因爲超時或中斷,該線程已經被取消
        CONDITION(-2):表明該線程被處於條件隊列,就是因爲調用了Condition.await而被阻塞
        PROPAGATE(-3):傳播共享鎖
        0:0代表無狀態
    */
    private Node enq(final Node node) {
        //無限循環
        for (;;) {
            // 獲取當前的尾部節點
            Node t = tail;
            // 如果爲空的情況
            if (t == null) { // 初始化處理
                // 通過CAS初始化
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //將引用的node的上一級改爲當前尾節點
                node.prev = t;
                // CAS比較將內存地址中的偏移量改爲node
                if (compareAndSetTail(t, node)) {
                    // 將當前的尾部節點也改爲node
                    t.next = node;
                    return t;
                }
            }
        }
    }
    
    
    
    /////////////////////////////////2.3//////////////////////////////
    //acquireQueued
    //acquireQueued的主要作用是把已經追加到隊列的線程節點(addWaiter方法
    //返回值)進行阻塞,但阻塞前又通過tryAccquire重試是否能獲得鎖,如果
    //重試成功能則無需阻塞,直接返回
    // 這裏需要注意的點:
    // 1. 喚醒的時候,如果頭節點已經被取消了,則會從tail中找出最前面的有效阻塞節點,然後喚醒
    // 2. 這裏的自旋只有在某個線程被喚醒,並且這個節點的前繼節點爲頭結點的同時,自旋纔會終止
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 注意:無限循環
            for (;;) {
                // 獲取前繼節點(也就是鏈表中的上一節點狀態)
                final Node p = node.predecessor();
                // 比較頭部是否相同
                // tryAcquire嘗試判斷當前線程是否爲搶佔鎖或者是否是重入鎖
                if (p == head && tryAcquire(arg)) {
                    // 設置頭部節點
                    setHead(node);
                    // 幫助GC清空對象
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 這一步很關鍵 2.3.1
                if (shouldParkAfterFailedAcquire(p, node) &&
                    // 2.3.2
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    
    // 2.3.1
    // 查詢當前線程的變化
    // 剛剛說過,會到這裏就是沒有搶到鎖唄,這個方法說的是:"當前線程沒有搶到鎖,是否需要掛起當前線程?"
    // 第一個參數是前驅節點,第二個參數纔是代表當前線程的節點
    // 概述: 當waitStatus == -1時表示需要被喚醒
    // 當返回false時表示不需要被喚醒
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 判斷前驅節點的狀態
        int ws = pred.waitStatus;
        
        // 前驅節點的 waitStatus == -1 ,說明前驅節點狀態正常,當前線程需要掛起,直接可以返回true
        // 也表示當前lock()鎖確實起作用了.
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
            
            
        // 前驅節點 waitStatus大於0 ,之前說過,大於0 說明前驅節點取消了排隊。這裏需要知道這點:
        // 進入阻塞隊列排隊的線程會被掛起,而喚醒的操作是由前驅節點完成的。
        // 所以下面這塊代碼說的是將當前節點的prev指向waitStatus<=0的節點,
        // 簡單說,就是爲了找個好爹,因爲你還得依賴它來喚醒呢,如果前驅節點取消了排隊,
        // 找前驅節點的前驅節點做爹,往前循環總能找到一個好爹的
        // 能進入到這裏的節點說明已經被取消了的,取消有幾種場景,其中就是超時
        // tryLock(超時時間),一旦超時會調用cancelAcquire方法,這個方法會將waitStatus設置成大於1的情況, 如果這個線程存在多個競爭的話,可能會超過1 
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                // 一直向前驅節點的上級節點查找,直到找到狀態爲0,也就是正常的線程
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            // 仔細想想,如果進入到這個分支意味着什麼
            // 前驅節點的waitStatus不等於-1和1,那也就是隻可能是0,-2,-3
            // 在我們前面的源碼中,都沒有看到有設置waitStatus的,所以每個新的node入隊時,waitStatu都是0
            // 用CAS將前驅節點的waitStatus設置爲Node.SIGNAL(也就是-1)
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    
    // 2.3.2 parkAndCheckInterrupt方法
    
    // 表示掛起當前線程
    // 這個方法很簡單,因爲前面返回true,所以需要掛起線程,這個方法就是負責掛起線程的
    // 這裏用了LockSupport.park(this)來掛起線程,然後就停在這裏了,等待被喚醒=======
    private final boolean parkAndCheckInterrupt() {
        // 直到被其他線程調用LockSupport.unpark喚醒
        LockSupport.park(this);
        // 判斷線程是否中斷,如果是被喚醒的線程,則會返回false
        return Thread.interrupted();
    }
    
    // 2. 接下來說說如果shouldParkAfterFailedAcquire(p, node)返回false的情況
  
   // 仔細看shouldParkAfterFailedAcquire(p, node),我們可以發現,其實第一次進來的時候,一般都不會返回true的,原因很簡單,前驅節點的waitStatus=-1是依賴於後繼節點設置的。也就是說,我都還沒給前驅設置-1呢,怎麼可能是true呢,但是要看到,這個方法是套在循環裏的,所以第二次進來的時候狀態就是-1了。
  
    // 解釋下爲什麼shouldParkAfterFailedAcquire(p, node)返回false的時候不直接掛起線程:
    // => 是爲了應對在經過這個方法後,node已經是head的直接後繼節點了。剩下的讀者自己想想吧。

總結 [lock方法做了哪些事情? 經過了哪些步驟?] 我們來還原步驟

  1. 初始化
  • 表示當前沒有搶佔現象,就是第一個線程第一次調用的時候是使用CAS將狀態從0改爲1
                // 判斷當前成狀態是否爲0,並且通過CAS去改變值爲1,如果成功,則綁定這個線程,標識爲獨佔鎖
                if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
  1. 搶佔鎖的過程 表示當前的鎖狀態不爲0的情況下
    代碼塊:
if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
  1. 搶佔鎖這塊代碼
final boolean nonfairTryAcquire(int acquires) {
  // 注意看清楚,這個是當前線程
  final Thread current = Thread.currentThread();
  // 這個是獲取目前資源池的數量
  int c = getState();
  if (c == 0) {
    // 等於0則搶佔
    if (compareAndSetState(0, acquires)) {
      setExclusiveOwnerThread(current);
      return true;
    }
  }
  // 又或者是同一個線程執行重入規則
  else if (current == getExclusiveOwnerThread()) {
    int nextc = c + acquires;
    if (nextc < 0) // overflow
      throw new Error("Maximum lock count exceeded");
    setState(nextc);
    return true;
  }
  return false;
}

tryAcquire:

  • 判斷當前線程和和鎖的擁有者是否爲同一個,如果是同一個的話則只是簡單的+1,並且設置爲state,所以通過setStatus修改,而非CAS,也就是說這段代碼實現了偏向鎖的功能,並且實現的非常漂亮。
  • 如果上面的鎖已經被搶佔,並且鎖的擁有者非當前線程,則開始將線程添加到一個無法獲得鎖的線程包裝鏈表中這個鏈表專門用於承裝沒有搶佔到鎖的線程,沒有搶到的則會在鏈表[阻塞隊列]的處於末端..[addWaiter方法]
    • 阻塞隊列:因爲 爭搶鎖的線程可能很多,但是只能有一個線程拿到鎖,其他線程必須等待,這個時候就需要一個queue來管理這些線程,AQS用的是一個FIFO的隊列,就是一個鏈表。每個node都持有後繼節點的引用,AQS採用了CLH鎖的變體來實現

acquireQueued

  • 通過自旋將已經加到阻塞隊列裏面的線程進行阻塞,阻塞前會判斷該節點的前繼節點是否爲head節點,如果是的則會嘗試進行一次搶佔,如果沒有成功,則會對該節點的前繼節點做判斷,是否爲有效節點<0;直到找到一個有效的停靠節點之後,纔開始阻塞
    • 阻塞線程和解除阻塞採用的是AQS的LockSupport.park(thread) 來掛起線程,用unpark來喚醒線程。
  • 一旦有線程被喚醒,則會回到自旋當中去繼續判斷該節點的前繼節點是否爲head。。。。直到爲head爲止

釋放鎖關鍵代碼

// 注意這裏的node是等於head節點的
private void unparkSuccessor(Node node) {
        // 獲取head節點的狀態
        int ws = node.waitStatus;
        // 判斷節點的狀態是否有效?有效的話,將狀態重置
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        // 這裏承接上面
        // 判斷head節點是否真的有效?萬一剛好上面bingfa取消了?
        // 判斷head節點是否有效?如果有效,則直接調用下面的喚醒方法
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
          // 能進入到這裏面說明head節點已經放棄了
            s = null;
          // 這裏開始從tail節點中找尋一個有效節點
          // 這裏注意咯,這裏是一直循環往上找!!!!!!
          // 找到上一個節點有效 還bu行?還得繼續往上找,直到找到最靠前bing且有效的節點
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        // 找到一個最有效的線程, 喚醒
        if (s != null)
            LockSupport.unpark(s.thread);
    }

喚醒的流程:

  • 調用到喚醒之後,找到一個有效的線程節點
    • 如果是head節點,直接回到3.搶佔代碼那塊,通過head節點匹配上直接喚醒,返回
    • 如果head節點放棄了呢?
      • 找到一個靠譜的最靠前的也就是head節點後面的(不管後幾位,因爲shouldParkAfterFailedAcquire方法遲早會將這個節點掛靠到head下面)下一位有效的節點
      • 這個線程被喚醒之後,它的前繼節點一定是head.所以滿足條件,進入嘗試獲取

非公平鎖的場景:

  • 假設有ABCDEF6個線程,A搶到的鎖,BCDEF只能在阻塞隊列中等待A釋放鎖的之後被喚醒.
  • 這時候A剛釋放完鎖之後,G這時候進來了,正常來說應該排隊在F後面的由B頂上的,但是,非公平的現象就出現了
  • 這時候G和B會同時搶佔鎖,誰先搶到誰就先上,如果G搶到了,那麼B就繼續在阻塞隊列中候着..

其實就是當前鎖已經被佔有的同時,其他線程進來,發現沒有鎖,準備去等待隊列裏面等待的時候,忽然鎖釋放掉了,這時候就會有多個線程進行競爭nonfairTryAcquire方法,這裏會通過CAS進行設置,誰先搶到誰就是持鎖人

參考文章:

http://blog.csdn.net/chen77716/article/details/6641477
https://hongjiev.github.io/2017/06/16/AbstractQueuedSynchronizer/

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