關於 ReentrantLock 中鎖 lock() 和解鎖 unlock() 的底層原理淺析

顏子歌發表於2020-12-22

關於 ReentrantLock 中鎖 lock() 和解鎖 unlock() 的底層原理淺析

如下程式碼,當我們在使用 ReentrantLock 進行加鎖和解鎖時,底層到底是如何幫助我們進行控制的啦?

    static Lock lock = new ReentrantLock();

    public static void main(String[] args) {

        // 使用兩個執行緒模擬多執行緒執行併發
        new Thread(() -> doBusiness(), "Thread-1").start();
        new Thread(() -> doBusiness(), "Thread-2").start();
    }

    private static void doBusiness() {
        try {
            lock.lock();
            System.out.println("需要加鎖的業務處理程式碼,防止併發異常");
        } finally {
            lock.unlock();
        }
    }

帶著這樣的疑問,我們先後跟進 lock()和unlock() 原始碼一探究竟

說明:

  1、在進行檢視 ReentrantLock 進行 lock() 加鎖和 unlock() 解鎖原始碼時,需要知道 LockSupport 類、瞭解自旋鎖以及連結串列相關知識。

  2、在分析過程中,假設第一個執行緒獲取到鎖的時候執行程式碼需要很長時間才釋放鎖,及在第二個第三個執行緒來獲取鎖的時候,第一個執行緒並沒有執行完成,沒有釋放鎖資源。

  3、在分析過程中,我們假設第一個執行緒就是最先進來獲取鎖的執行緒,那麼第二個第三個執行緒也是依次進入的,不會存在第三個執行緒先於第二個執行緒(即第三個執行緒如果先於第二個執行緒發生,那麼第三個執行緒就是我們下面描述的第二個執行緒)

 

一、 lock() 方法

1、檢視lock()方法原始碼

    public void lock() {
        sync.lock();
    }

  從上面可以看出 ReentrantLock 的 lock() 方法呼叫的是 sync 這個物件的 lock() 方法,而 Sync 就是一個實現了抽象類AQS(AbstractQueuedSynchronizer) 抽象佇列同步器的一個子類,繼續跟進程式碼(說明:ReentrantLock 分為公平鎖和非公平鎖,如果無參構造器建立鎖預設是非公平鎖,我們按照非公平鎖的程式碼來講解)

  1.1 關於Sync子類的原始碼

abstract static class Sync extends AbstractQueuedSynchronizer { 
    // 此處省略具體實現AbstractQueuedSynchronizer 類的多個方法
}

  這裡需要說明的是 AbstractQueuedSynchronizer 抽象佇列同步器底層是一個通過Node實現的雙向連結串列,該抽象同步器有三個屬性 head 頭節點tail 尾節點 和 state 狀態值。

    屬性1:head——註釋英文翻譯:等待佇列的頭部,懶載入,用於初始化,當呼叫 setHead() 方法的時候會對 head 進行修改。注:如果 head 節點存在,則 head 節點的 waitStatus 狀態值用於保證其不變成 CANCELLED(取消,值為1) 狀態

    /**
     * Head of the wait queue, lazily initialized.  Except for
     * initialization, it is modified only via method setHead.  Note:
     * If head exists, its waitStatus is guaranteed not to be
     * CANCELLED.
     */
    private transient volatile Node head;

    屬性2: tail——tail節點是等待佇列的尾部,懶載入,在呼叫 enq() 方法新增一個新的 node 到等待佇列的時候會修改 tail 節點。

    /**
     * Tail of the wait queue, lazily initialized.  Modified only via
     * method enq to add new wait node.
     */
    private transient volatile Node tail;

    屬性3:state——用於同步的狀態碼。如果 state 該值為0,則表示沒有其他執行緒獲取到鎖,如果該值大於1則表示已經被某執行緒獲取到了鎖,該值可以是2、3、4,用該值來處理重入鎖(遞迴鎖)的邏輯。

    /**
     * The synchronization state.
     */
    private volatile int state;

  1.2 上面 Sync 類使用 Node來作為雙向佇列的具體儲存值和狀態的載體,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; // 排他鎖模式(也叫互斥鎖)

        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1; // Node執行緒等待取消,不再參與鎖競爭,處於這種狀態的Node會被踢出佇列,被GC回收
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1; // 表明Node執行緒需要被喚醒,可以競爭鎖
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2; // 表示這個Node執行緒在條件佇列中,因為等待某個條件而被阻塞 
        /** waitStatus value to indicate the next acquireShared should unconditionally propagate */
        static final int PROPAGATE = -3; // 使用在共享模式頭Node有可能處於這種狀態, 表示鎖的下一次獲取可以無條件傳播

        volatile int waitStatus; // 預設初始狀態為0,所有新增node節點的初始狀態都是0

        volatile Node prev; // 前驅Node節點

        volatile Node next; // 後繼Node節點

        /** The thread that enqueued this node.  Initialized on construction and nulled out after use.*/
        volatile Thread thread; // 當前執行緒和節點進行繫結,通過構造器初始化Thread,在使用的時候將當前執行緒替換原有的null值

        // 省略部分程式碼

  說明:Sync 通過Node節點構建佇列,Node節點使用prev和next節點來行程雙向佇列,使用prev來關聯上一個節點,使用next來關聯下一個節點,每一個node節點和一個thread執行緒進行繫結,用來表示當前執行緒在阻塞佇列中的具體位置和狀態 waitStatus

 

2、上面的 sync.lock() 繼續跟進原始碼(非公平鎖):

    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

說明:上面程式碼說明,如果 compareAndSetState(0, 1) 為 true ,則執行 setExclusiveOwnerThread(Thread.currentThread()) ,否則執行 acquire(1);

  2.1 compareAndSetState(0, 1) 底層使用unsafe類完成CAS操作,意思就是判斷當前state狀態是否為0,如果為零則將該值修改為1,並返回true;state不為0,則無法將該值修改為1,返回false。

    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

  2.2 假如第1個執行緒進來的時候 compareAndSetState(0, 1) 肯定執行成功,state 狀態會從0變成1,同時返回true,執行 setExclusiveOwnerThread(Thread.currentThread()) 方法:

    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

  setExclusiveOwnerThread(Thread.currentThread()) 表示將當前 Sync 物件和當前執行緒繫結,意思是表明:當前對內同步器執行的執行緒為 thread,該 thread 獲取了鎖正在執行。

  2.3 假如進來的執行緒為第2個,並且第一個執行緒還在執行沒有釋放鎖,那麼第2個執行緒就會執行 acquire(1)方法:

    public final void acquire(int arg) {
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

  進入到該方法中發現,需要通過 !tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 兩個方法判斷是否需要執行 selfInterrupt();

    (1)先執行tryAcquire(arg)這個方法進行判斷

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
        /**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         */
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
       // 獲取state狀態,因為第一個執行緒進來的時候只要還沒有執行完就已經將state設定為1了(即:2.1步)
int c = getState();
       // 再次判斷之前獲取鎖的執行緒是否已經釋放鎖了
if (c == 0) {
// 如果之前的執行緒已經釋放鎖,那麼當前執行緒進來就將狀態改為1,並且設定當前佔用鎖的執行緒為自身
if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } }
       // 判斷當前佔用鎖的執行緒是不是就是我自身,如果是我自身,這將State在原值的基礎上進行加1,來處理重入鎖邏輯
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(arg) 直接放回false,那麼 !tryAcquire(arg) 就為true,需要判斷第二個方法 acquireQueued(addWaiter(Node.EXCLUSIVE), arg),第二個方法先執行addWaiter(Node.EXCLUSIVE),及新增等待執行緒進入佇列

    (2)新增等待執行緒到同步阻塞佇列中

    private Node addWaiter(Node mode) {
     // 將當前執行緒和node節點進行繫結,設定模式為排他鎖模式 Node node
= new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail;// 第二個執行緒也就是第一次進來該方法的執行緒,tail肯定是null if (pred != null) { // 如果tail尾節點不為空,表示第3、4、5次進來的執行緒 node.prev = pred; // 那麼就將當前進來的執行緒節點的 prev 節點指向之前的尾節點 if (compareAndSetTail(pred, node)) { // 通過比較並交換,如果當前尾節點在設定過程中沒有被其他執行緒搶先操作,那麼就將當前節點設定為tail尾節點 pred.next = node; // 將以前尾節點的下一個節點指向當前節點(新的尾節點) return node; } } enq(node); // 如果為第二個執行緒進來,就是上面的 pred != null 成立沒有執行,直接執行enq()方法 return node; }
    private Node enq(final Node node) {
        for (;;) { // 一直迴圈檢查,相當於自旋鎖
            Node t = tail;
            if (t == null) { // Must initialize
                   // 第二個執行緒的第一次進來肯定先迴圈進入該方法,這時設定頭結點,該頭結點一般被稱為哨兵節點,並且頭和尾都指向該節點
if (compareAndSetHead(new Node())) tail = head; } else {
          // 1、第二個執行緒在第二次迴圈時將進入else 方法中,將該節點掛在哨兵節點(頭結點)後,並且尾節點指向該節點,並且將該節點返回(該節點有prev資訊) node.prev
= t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }

    如上在執行 enq(final Node node) 結束,並且返回新增了第二個執行緒node節點的時候, addWaiter(Node mode) 方法會繼續向上返回

    或者

    如果是新增第3、4個執行緒直接走 addWaiter(Node mode) 方法中的 if 流程直接新增返回

    都將到2.3 步,執行acquireQueued(final Node node, int arg),再次貼原始碼

    public final void acquire(int arg) {
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

    (3)即下一步就會執行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 方法:

    注:上面的流程是將後面的執行緒加入到了同步阻塞佇列中,下面的方法第一個if (p == head && tryAcquire(arg))則是看同步阻塞佇列的第一條阻塞執行緒是否可以獲取到鎖,如果能夠獲取到鎖就修改相應連結串列結構,第二個if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()即將發生執行緒阻塞

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
          // 自旋鎖,如果為第二個執行緒,那麼 p 就是 head 哨兵節點
final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) {
            // 上面的 if 表明如果當前執行緒為同步阻塞佇列中的第一個執行緒,那麼就再次試圖獲取鎖 tryAcquire(),如果獲取成功,則修改同步阻塞佇列 setHead(node); // 將head頭結點(哨兵節點)設定為已經獲取鎖的執行緒node,並將該node的Theread 設定為空 p.next
= null; // help GC 取消和之前哨兵節點的關聯,便於垃圾回收器對之前資料的回收 failed = false; return interrupted; }
          // 如果第二個執行緒沒有獲取到鎖(同步阻塞佇列中的第一個執行緒),那麼就需要執行下面兩個方法,註標的方法會讓當前未獲取到鎖的執行緒阻塞
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
    private void setHead(Node node) {
        // 將哨兵節點往後移,並且將 thread 設定為空,取消和以前哨兵節點的關聯,並於垃圾回收器回收
        head = node;
        node.thread = null;
        node.prev = null;
    }

    shouldParkAfterFailedAcquire(p, node)這個方法將哨兵佇列的狀態設定為待喚醒狀態

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
     // 該方法第一次進來 pred為哨兵節點,ws為哨兵節點的初始0狀態
     // 該方法第二次進來 pred為哨兵節點,ws為哨兵節點的狀態-1狀態
     // 該方法第三、四次進來就為連結串列的倒數第二個節點,ws為倒數第二個節點的狀態
int ws = pred.waitStatus;
    
if (ws == Node.SIGNAL) /* * This node has already set status asking a release * to signal it, so it can safely park. */
       // 如上第二次進來的時候哨兵節點的狀態就是-1,此時返回true
return true; if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do {
          // 如果ws及倒數第二個節點是取消狀態,那麼通過雙向連結串列向前找倒數第三個,第四關節點,直到向前找到最近一個狀態不是取消的node節點,並把當前節點掛在該節點後 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. */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); // 將頭結點(哨兵節點)設定成待喚醒狀態,第一次進來的時候 有0——>-1 然後繼續執行返回false } return false; }
    parkAndCheckInterrupt()這個方法會讓當前執行緒阻塞
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this); // LockSupport.park()會導致當前執行緒阻塞,直到某個執行緒呼叫unpark()方法
        return Thread.interrupted();
    }

    那麼在lock()方法執行時,只要第一個執行緒沒有unlock()釋放鎖,其他所有執行緒都會加入同步阻塞佇列中,該佇列中記錄了阻塞執行緒的順序,在加入同步阻塞佇列前有多次機會可以搶先執行(非公平鎖),如果沒有被執行到,那麼加入同步阻塞佇列後,就只有頭部節點(哨兵節點)後的阻塞執行緒有機會獲取到鎖進行邏輯處理。再次檢視該方法: 

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    // if 表明只有頭部節點(哨兵節點)後的節點在放入同步阻塞佇列前可以獲取鎖
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) // 所有執行緒都被阻塞在這個方法處
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }        

 

二、unlock()方法

1、unlock原始碼

    public void unlock() {
        sync.release(1);
    }

  同樣是呼叫的同步阻塞佇列的方法 sync.release(1),跟進檢視原始碼:

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

2、檢視tryRelease()方法:

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                // 如果不是自身鎖物件呼叫unlock()方法的話,就報異常
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                // 如果標誌位已經為0,表示重入鎖已經全部釋放,這將當前獲取鎖的執行緒設定為null,以便其他執行緒進行加鎖
                setExclusiveOwnerThread(null);
            }
            // 更新重入鎖解鎖到達的次數,如果C不為0,表示還有重入鎖unlock()沒有呼叫完
            setState(c);
            return free;
        }        

3、如果tryRelease()方法成功執行,表示之前獲取鎖的執行緒已經執行完所有需要同步的程式碼(重入鎖也完全退出),那麼就需要喚醒同步阻塞佇列中的第一個等待的執行緒(也是等待最久的執行緒),執行unparkSuccessor(h)方法:

    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        // 先獲取頭結點(哨兵節點)的waitStatus狀態,如果小於0,則可以獲取鎖,並將waitStatus的狀態設定為0
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            // 如果哨兵節點的下一個節點為null,或者狀態為1表示已經取消,則依次迴圈尋找後面節點,直至找到一個waitStatus<0的節點,並將該節點設定為需要獲取鎖的節點
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            // 將該node節點的執行緒解鎖,允許它去獲取鎖,然後執行業務邏輯
            LockSupport.unpark(s.thread);
    }

 

三、unlock()方法呼叫後,會到lock()方法阻塞的地方,完成喚醒工作

1、在上面方法 unparkSuccessor(Node node) 中執行完 LockSupport.unpark(s.thread) 後在同步阻塞佇列後的第一個 node 關聯的執行緒將被喚醒,即unlock()方法程式碼執行完,將會到lock() 原始碼解析的 2.3 步裡,第三次貼該處原始碼:

    public final void acquire(int arg) {
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

2、在上面放大的紅色方法中,之前上面lock()原始碼講了當中所有執行緒都被阻塞了,如下面原始碼紅色標記的地方:

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

3、所有未獲取到鎖的執行緒都在 parkAndCheckInterrupt() 方法處阻塞著,所以我們即將喚醒的哨兵節點後的第一個阻塞執行緒也是在該處阻塞著,在執行完 unlock() 原始碼步驟第3步unparkSuccessor(Node node)中的方法,則將返回到之前阻塞執行緒的這個方法 parkAndCheckInterrupt()的這行程式碼 LockSupport.park(this) 的下一步執行 Thread.interrupted(),因為執行緒沒有被打斷,所以返回false,故acquireQueued(final Node node, int arg)方法中繼續輪訓再次嘗試acquireQueued(final Node node, int arg)獲取鎖,因為第一個執行緒已經釋放鎖,所以第二個執行緒可以獲取鎖了,並在執行完後返回interrupted為false,表示執行緒不是被中斷的。理,其他執行緒也在parkAndCheckInterrupt()這個方法中中斷著,等待被第二個執行緒喚醒。

 

總結:

  在第一個 A 執行緒 lock() 獲取到鎖後,第一個執行緒會在底層的同步阻塞佇列中設定鎖狀態 state 為1(如果重入鎖多次獲取 state 每次加1),並設定擁有當前鎖的執行緒為自身A執行緒,其他執行緒 B/C/D 來獲取鎖的時候就會比較鎖狀態是否為0,如果不為0,表示已經被獲取了鎖,再次比較獲取鎖的執行緒是否為自身,如果為自身則對 state 加1(滿足重入鎖的規則),否則這將當前未獲取到鎖的執行緒放入同步阻塞佇列中,在放入的過程中,需要設定 head 哨兵節點和 tail 尾節點,以及相應的 waitStatus 狀態,並且在放入過程中需要設定當前節點以及先關節點的 prev 和 next 節點,從而達到雙向佇列的效果,存放到阻塞佇列後,執行緒會被阻塞到這樣一個方法中 parkAndCheckInterrupt(),等待被喚醒。

  在第一個 A 執行緒執行完畢,呼叫 unlock() 解鎖後,unlock() 方法會從同步阻塞佇列的哨兵節點後的第一個節點獲取等待解鎖的執行緒B,並將其解鎖,然後就會到B阻塞的方法 parkAndCheckInterrupt() 來繼續執行,呼叫 selfInterrupt()方法,最終呼叫底層的 c語言的喚醒方法,使得 B 執行緒完成 lock() 方法獲取到鎖,然後執行業務邏輯。其他執行緒以此類推,依次發生阻塞和喚醒。

 

根據原始碼總結的 lock() 和 unlock() 的原理,歡迎大神批評指正,如有剛好的見解,也歡迎大家相互交流。

相關文章