java原始碼-ReentrantLock之FairSync

晴天哥發表於2018-08-30

開篇

 這篇文章主要是講解FairSync公平鎖的原始碼分析,整個內容分為加鎖過程、解鎖過程,CLH佇列等概念。
 首先一直困擾我的CLH佇列的CLH的縮寫我終於明白,看似三個人的人名的首字元縮寫”CLH” (Craig, Landin, andHagersten)。
 加鎖過程主要核心邏輯在於嘗試獲取鎖,獲鎖失敗後進入等待佇列,以及進入等待佇列的過程是需要進行多次迴圈判斷的。
 解鎖過程相對加鎖過程會簡單許多,核心邏輯在釋放鎖、喚醒下一個等待執行緒兩個過程。
  CLH的概念在加鎖過程已經提及了,可以一併看看。

java原始碼 – ReentrantLock
java原始碼 – ReentrantLock之FairSync
java原始碼 – ReentrantLock之NonfairSync
java原始碼 – ReentrantLock圖解加鎖過程

加鎖過程

ReentrantLock的的鎖過程如下:

  • 1、先嚐試獲取鎖,通過tryAcquire()實現。
  • 2、獲取鎖失敗後,執行緒被包裝成Node物件後新增到CLH佇列,通過addWaiter()實現。
  • 3、新增CLH佇列後,逐步的去執行CLH佇列的執行緒,如果當前執行緒獲取到了鎖,則返回;否則,當前執行緒進行休眠,直到喚醒並重新獲取鎖了才返回。

tryAcquire的操作流程
1、如果鎖未佔用的情況下:判斷當前執行緒是否處於CLH的首位,如果位於首位就通過原子更新操作設定鎖佔用。
2、如果鎖被佔用的情況下:判斷當前執行緒是否是佔用鎖執行緒,如果是則實現鎖的可重入功能,設定鎖佔用次數。

    static final class FairSync extends Sync {
        
        // lock的入口,內部呼叫acquire方法實現加鎖操作
        final void lock() {
            // lock的入口
            acquire(1);
        }

      public final void acquire(int arg) {
          // 第一步嘗試獲取鎖,成功則返回
          // 獲取鎖失敗後通過addWaiter新增到CLH佇列的末尾
          // 通過acquireQueued判斷是否輪到自己喚醒了
          // 可以理解為之前沒獲取鎖但是等執行到這裡的時候可能鎖已經釋放了
          if (!tryAcquire(arg) &&
              acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
              selfInterrupt();
      }
  
        // acquires的引數值為1
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            // 獲取當前鎖狀態,0表示鎖未佔用,>0表示被佔用
            int c = getState();
            if (c == 0) {
                // 首先判斷是不是CLH佇列的第一個元素,沒有祖先則表示第一個元素
                // 然後從unsafe把state設定為1,表示鎖被佔用
                // 設定鎖佔用執行緒為當前執行緒
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    // 設定鎖佔用執行緒為當前執行緒
                    setExclusiveOwnerThread(current);
                    // 返回鎖佔用成功
                    return true;
                }
            }
            // 判斷鎖佔用執行緒是不是本執行緒,說明是可重入鎖
            else if (current == getExclusiveOwnerThread()) {
                // 重入鎖增加鎖定次數
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                // 設定state會佔用次數
                setState(nextc);
                // 返回鎖佔用成功
                return true;
            }

            // 否則返回鎖佔用失敗
            return false;
        }
    }

acquire的操作流程

  • 1、第一步通過tryAcquire()嘗試獲取鎖,成功則返回
  • 2、獲取鎖失敗後通過addWaiter新增到CLH佇列的末尾
  • 3、新增CLH佇列後,通過acquireQueued()方法逐步的去執行CLH佇列的執行緒,如果當前執行緒獲取到了鎖則返回;否則當前執行緒進行休眠,直到喚醒並重新獲取鎖後返回。
    public final void acquire(int arg) {
        // 第一步嘗試獲取鎖,成功則返回
        // 獲取鎖失敗後通過addWaiter新增到CLH佇列的末尾
        // 通過acquireQueued判斷是否輪到自己喚醒了
        // 可以理解為之前沒獲取鎖但是等執行到這裡的時候可能鎖已經釋放了
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

addWaiter的操作流程
1、將當前執行緒包裝成Node物件。
2、先嚐試通過快速失敗法嘗試在CLH隊尾插入Node物件
3、如果快速插入失敗後那麼就通過enq方法在CLH隊尾插入Node物件

    private Node addWaiter(Node mode) {
        // 將執行緒包裝成為Node物件,便於新增CLH佇列
        Node node = new Node(Thread.currentThread(), mode);

        // 先嚐試快速插入到CLH隊尾,插入成功就返回Node物件
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }

        // 快速插入CLH隊尾失敗後,通過enq方法實現
        enq(node);
        return node;
    }

    // 將Node節點插入CLH隊尾的實現
    private Node enq(final Node node) {
        for (;;) {
           
            Node t = tail;

            // 如果CLH佇列為空,那麼設定Head和Tail都為Node
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                // 通過unsafe來保證Node插入隊尾
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

acquireQueued的操作流程

  • 1、如果當前節點Node的前驅節點屬於head,當前節點屬於老二地位通過tryAcquire()嘗試獲取鎖,獲取成功後那麼就釋放原head節點(可以理解為head已經釋放鎖然後從CLH刪除),把當前節點設定為head節點。
  • 2、通過shouldParkAfterFailedAcquire()方法判斷Node代表的執行緒是否進入waiting狀態,直到被unpark()。
  • 3、parkAndCheckInterrupt()方法將當前執行緒進入waiting狀態。
  • 4、休眠執行緒被喚醒的時候會執行 if (p == head && tryAcquire(arg))邏輯判斷
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                // 先判斷節點的祖先
                final Node p = node.predecessor();
                // 如果前驅是head,即該結點已成老二,
                // 那麼便有資格去嘗試獲取資源,
                // tryAcquire成功說明head已經釋放鎖
                // 休眠執行緒被喚醒的時候會繼續執行這裡
                if (p == head && tryAcquire(arg)) {
                    // 設定當前節點為head節點
                    setHead(node);
                     // 釋放原head節點用於gc回收
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }

                // 如果自己可以休息了,就進入waiting狀態,直到被unpark()
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    //如果等待過程中被中斷過,哪怕只有那麼一次,就將interrupted標記為true
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

shouldParkAfterFailedAcquire的操作流程

  • 1、如果前置節點處於SIGNAL狀態,那麼當前執行緒進入阻塞狀態,返回true
  • 2、如果前置節點處於ws>0也就是取消狀態,那麼當前執行緒節點就往前查詢第一個狀態處於ws<=0的節點
  • 3、如果前置狀態ws=0的節點,那麼就把前置節點設定為SIGNAL狀態
  • 4、整個shouldParkAfterFailedAcquire函式是在for()迴圈當中迴圈執行的,我們可以想象按照步驟2->3->1的順序執行,按照前置遍歷尋找合適的前置節點,接著發現前置節點ws狀態為0後重新設定為SIGNAL,最後發現前置節點狀態為SINGAL後休眠執行緒自身。
  • 5、執行緒從執行態進入waiting狀態其實也是經歷了一系列的處理過程的。
    // shouldParkAfterFailedAcquire外層for迴圈呼叫
    // 第一次設定Node前置節點狀態為SIGNAL
    // 下一次迴圈就前置節點莊為SIGNAL,那麼執行緒自身就需要被阻塞了
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
       
       // 如果前繼節點是SIGNAL狀態,則意味這當前執行緒需要被阻塞。此時,返回true。
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            return true;

        // ws>0代表執行緒被取消了
        // static final int CANCELLED =  1;
        //  waitStatus value to indicate thread has cancelled
        if (ws > 0) {
            // 如果前驅處於取消狀態,那就一直往前找,直到找到最近一個正常等待的狀態,並排在它的後邊
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 如果前繼節點為“0”或者“共享鎖”狀態,則設定前繼節點為SIGNAL狀態。
            // 狀態為0的情況只可能是初始化的時候的預設值
            // 當前執行緒進入等待狀態的時候需要設定前置狀態為SIGNAL
            // SIGNAL狀態表示後置執行緒需要被喚醒
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

    private final boolean parkAndCheckInterrupt() {
        // parkAndCheckInterrupt()的作用是阻塞當前執行緒,並且返回“執行緒被喚醒之後”的中斷狀態。
        LockSupport.park(this);
        return Thread.interrupted();
    }

Node的介紹

  • 1、Node節點作為CLH佇列的節點元素,內部包含執行緒物件
  • 2、Node節點包含多種狀態,每種狀態都在原始碼中註釋了,預設初始化應該為0
  • 3、Node節點是一個雙向列表的節點,包含前置和後置節點的指標
  • 4、Node節點處於AbstractQueuedSynchronizer類當中,其中AbstractQueuedSynchronizer包含state變數標記是否處於鎖狀態
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    protected AbstractQueuedSynchronizer() { }

    /**
     *      +------+  prev +-----+       +-----+
     * head |      | <---- |     | <---- |     |  tail
     *      +------+       +-----+       +-----+
     */
    static final class Node {

        static final Node SHARED = new Node();

        static final Node EXCLUSIVE = null;
        // 當前執行緒已被取消
        static final int CANCELLED =  1;
        // “當前執行緒的後繼執行緒需要被unpark(喚醒)”。
        // 一般發生情況是:當前執行緒的後繼執行緒處於阻塞狀態,
        // 而當前執行緒被release或cancel掉,因此需要喚醒當前執行緒的後繼執行緒。
        static final int SIGNAL    = -1;
        // 當前執行緒(處在Condition休眠狀態)在等待Condition喚醒
        static final int CONDITION = -2;
        // (共享鎖)其它執行緒獲取到“共享鎖”,狀態為0表示當前執行緒不屬於上面的任何一種狀態。
        static final int PROPAGATE = -3;
        
        volatile int waitStatus;

        volatile Node prev;

        volatile Node next;

        volatile Thread thread;

        Node nextWaiter;

        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

    private transient volatile Node head;

    private transient volatile Node tail;

    private volatile int state;
}

解鎖過程

release過程

  • 1、通過tryRelease()方法嘗試讓當前執行緒釋放鎖物件
  • 2、通過unparkSuccessor()方法設定當前節點狀態ws=0並且喚醒CLH佇列中的下一個等待執行緒
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

tryRelease過程

  • 1、如果佔用鎖執行緒非當前執行緒直接拋異常
  • 2、遞減鎖計數後如果值為0那麼就釋放當前鎖佔用者
  • 3、更新鎖狀態為未佔用,即state為0
     protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
         boolean free = false;
         if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
         }
        setState(c);
        return free;
    }

unparkSuccessor過程
1、設定當前Node狀態為0
2、尋找下一個等待執行緒節點來喚醒等待執行緒並通過LockSupport.unpark()喚醒執行緒
3、尋找下一個等待執行緒,如果當前Node的下一個節點符合狀態就直接進行喚醒,否則從隊尾開始進行倒序查詢,找到最優先的執行緒進行喚醒。

    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 找到狀態<0的執行緒進行喚醒
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }

        if (s != null)
            LockSupport.unpark(s.thread);
    }

##參考文章
Java多執行緒:AQS原始碼分析


相關文章