ReentrantLock原始碼分析

辣雞小籃子發表於2020-09-06

ReentranLock從字面上理解就是可重入鎖,它支援同一個執行緒對資源的重複加鎖,也是我們平時在處理java併發情況下用的最多的同步元件之一(還有volatile,synchronized等)。
一般ReentrantLock的使用方式如下:

public class LockDemo {
    private Lock lock = new ReentrantLock();
    private int cnt = 0;
 
    public void setCnt(){
        lock.lock();
        try{
            //..........
            cnt++;
            //..........
        } finally {
             lock.unlock();
        }
    }
}

要了解一個類的功能,從它的內部方法與成員變數看起。


1593035-6c1dcbd7d48f9bfc.png

ReentranLock是通過Sync及其子類來實現的同步控制。也就是說ReentrantLock的同步功能是由Sync代理的。同時,ReentranLock也是通過FairSync與NonfairSync來支援ReentranLock在獲取鎖時的公平與非公平性選擇。實際上,JavaDoc裡對Sync的解釋已經很清楚了。

/**
     * Base of synchronization control for this lock. Subclassed
     * into fair and nonfair versions below. Uses AQS state to
     * represent the number of holds on the lock.
     */
    abstract static class Sync extends AbstractQueuedSynchronizer

一. 公平與非公平鎖

公平鎖與非公平鎖的區別:

公平鎖 多執行緒併發獲取鎖時,按照等待時間排序獲得鎖,等待時間最久的執行緒先獲得鎖
非公平鎖 獲取鎖時不考慮排隊問題,直接嘗試獲取鎖

我們可以沿著ReentrantLock的呼叫鏈來簡單分析下公平鎖與非公平鎖的實現區別。

1.lock方法原始碼分析

首先,ReentrantLock會呼叫lock方法,該方法只是簡單的呼叫成員變數sync的lock方法。

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

在這裡,由於sync的不同,會出現分支,FairSync與NonfairSync都有自己的分支。預設情況下,ReentrantLock是非公平鎖,而為了使用公平鎖,可以在初始化ReentrantLock時傳入引數進行選擇。

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

我們先看一下非公平鎖的分支:

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

先是直接嘗試通過CAS原子的設定state變數[如果state變數為0則將其設定為1,設定成功表明當前執行緒成功獲取了鎖],成功後設定當前執行緒為鎖的持有者,否則繼續呼叫AbstractQueuedSynchronizer的acquire方法。
而對於公平鎖來說,十分簡單,直接呼叫AbstractQueuedSynchronizer的acquire方法。

 final void lock() {
            acquire(1);
        }

FairSync與NonfairSync在acquire方法方法出匯合。繼續追蹤AbstractQueuedSynchronizer的acquire方法:

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

該方法的基本邏輯是:先呼叫tryAcquire方法嘗試獲取鎖,如果失敗,會構造一個獨佔的Node節點加入等待佇列。對於tryAcquire方法,基於FairSync與NonfairSync又有兩種實現。對於非公平鎖,會執行該方法:

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();//獲取狀態變數
            if (c == 0) {//表明沒有執行緒佔有該同步狀態
                if (compareAndSetState(0, acquires)) {//以原子方式設定該同步狀態
                    setExclusiveOwnerThread(current);//該執行緒擁有該FairSync同步狀態
                    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;
        }

而對於公平鎖,該方法則是這樣:

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                //先判斷該執行緒節點是否是佇列的頭結點
                //是則以原子方式設定同步狀態,獲取鎖
                //否則失敗返回
                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");
                setState(nextc);
                return true;
            }
            return false;
        }

2.unlock方法原始碼分析

ReentrantLock呼叫unlock方法:

public void unlock() {
        sync.release(1);//每次呼叫unlock方法,只對state變數減1操作,所以多次加鎖後需要多次解鎖
    }

由sync呼叫release方法釋放鎖。

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);//喚醒頭結點的後繼節點
            return true;
        }
        return false;
    }
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);//state減為0,表明目前沒有執行緒持有該鎖
            }
            setState(c);
            return free;
        }

我們可以觀察到,對AQS的state成員變數的操作都是不用加鎖的,原因在於state是一個volatile變數,可以從語言層面保持改變數線上程間的可見性。

 private volatile int state;
protected final int getState() {
        return state;
    }

到了這裡,我們可以總結一下公平鎖與非公平鎖的實現差別了:

  • 非公平鎖一開始會直接嘗試設定獲取同步狀態變數,獲取鎖
  • 在tryAcquire方法中,公平鎖會先判斷當前執行緒是否為在鎖的等待佇列的頭結點,由於該佇列是一個FIFO佇列,故這樣判斷可以實現公平鎖鎖追求的公平性-即等待時間最長的鎖先獲得鎖。
    總結:
  • 我們平時使用ReentrantLock時,預設是使用非公平鎖,因為在實際情況中,公平鎖往往沒有非公平鎖的效率高。非公平鎖的吞吐量會更高一些。
  • 公平鎖機制能夠減少'飢餓'的發生,按佇列順序,排隊的執行緒總能夠得到鎖。而非公平鎖可能導致在佇列裡的某些執行緒長時間無法獲取鎖。

二. AbstractQueuedSynchronizer的實現分析

在前面沿著ReentrantLock的呼叫鏈來分析公平鎖與非公平鎖的實現時,我們發現ReentrantLock的很多工作最終都是由AbstractQueuedSynchronizer類(以後稱AQS)完成的,比如FIFO同步佇列的管理操作、同步狀態變數的設定、CAS操作等都是由AQS完成的,AQS可以說是ReentrantLock實現同步功能的最基礎的框架類(其實,通過分析JDK中concurrent包裡提供的其它同步工具,我們會發現它們基本都把同步工作交給了AQS稱它為基礎框架類並不為過)。下面我們就通過原始碼的分析來了解AQS是如何做到這些工作的。
首先,我們可以看一下,所有的Sync都是繼承自AQS的。

abstract static class Sync extends AbstractQueuedSynchronizer

這一系列Sync類的繼承關係如圖 :


1593035-52fb1179f1ae9534.png

AQS裡的方法特別多,我們從AQS的幾個重要的成員變數看起,下圖是AQS的Outline,紅框標記的head,tail兩個Node節點,根據前面的瞭解,我們馬上就能聯想到,這就是AQS管理的FIFO等待佇列,用來處理獲取鎖狀態失敗的執行緒-獲取鎖狀態失敗的執行緒會被放入該佇列,等待再次嘗試獲取鎖。而state成員變數,代表著鎖的同步狀態,一個執行緒成功獲得鎖,這個行為的實質就是該執行緒成功的設定了state變數的狀態。


1593035-9019ab69ac0f29b9.png

下面我們詳細分析一下AQS是如何管理等待佇列的。
首先,我們要看一下Node節點這個資料結構是如何的,因為正是它構成了AQS的等待佇列。Node主要有一下幾個成員變數:
static final class Node {
    volatile int waitStatus;
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
    //......
}

簡單分析一下,一個waitStatus表示Node節點的一些狀態,pre/next表示該佇列是由雙向連結串列組成,thread表示是該執行緒入隊等待獲取鎖。對於waitStatus,該Node節點規定了6中狀態。

SHARED 表示節點處於共享模式,該模式會在AQS提供的acquire/releaseShared介面中使用,而該介面又會在能夠共享的同步元件中使用,如讀寫鎖中的讀鎖等。
EXCLUSIVE 表示節點處於獨佔模式,ReentrantLock就是使用這種模式。
CANCELLED(1) 由於同步佇列中 等待的執行緒等待超時或者被中斷,需要從同步佇列中取消等待,節點進入該狀態不會變化。
SIGNAL(-1) 後續節點處於等待狀態,而當前節點的執行緒如果釋放了同步狀態或被取消,將會會通知後繼節點,是後繼節點的執行緒得意繼續執行。
CONDITION(-2) 節點在等待佇列中,節點 執行緒等待在Condition上,當其他執行緒對Condition呼叫了signal方法時,該節點將會從等待佇列中轉移到同步佇列 ,加入到對同步狀態的獲取中。
PROPAGATE(-3) 表示下一次共享式 同步狀態獲取將會 無條件被傳播下去

1.AQS的acquire方法的分析

根據AQS中的 acquire方法,沒有成功獲取同步狀態的執行緒會被加入同步等待佇列的尾部。具體步驟是:

  • 先建立執行緒的節點並加入同步佇列
 private Node addWaiter(Node mode) {
        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方式設定佇列的尾節點
            //成功則設定該節點前、後向指標
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);//CAS方式設定等待佇列尾節點失敗
        return node;
    }
    //入隊
    private Node enq(final Node node) {
        for (;;) {//自旋CAS設定尾節點
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
  • 節點入隊後,自旋嘗試獲取同步狀態
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);
        }
    }
  • 前驅節點非佇列的頭結點,判斷前驅結點waitStatus為SIGNAL,等待該節點釋放鎖
//判斷node節點關聯的執行緒是否可以阻塞,判斷標準就是node的前置節點pred是否處於SIGNAL狀態
//如果是,則當前節點node可以睡眠等待前置節點執行緒釋放鎖來喚醒自己(unparking)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        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.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.node
             */
            do {
                node.prev = pred = pred.prev;//節點的前置節點處於cancel狀態,需要迴圈跳過這些節點
            } 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);
        }
        return false;
} 
  • 前驅節點處於SIGNAL狀態,node節點可以睡眠
private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }
//該方法會是當前執行緒暫時不被排程,直到發生三種情況:
//1.別的執行緒對該執行緒呼叫unpark操作
//2.別的執行緒終端該執行緒
//3.莫名返回
public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        unsafe.park(false, 0L);//呼叫native方法park阻塞當前執行緒
        setBlocker(t, null);
    }
 /**
     * Block current thread, returning when a balancing
     * <tt>unpark</tt> occurs, or a balancing <tt>unpark</tt> has
     * already occurred, or the thread is interrupted, or, if not
     * absolute and time is not zero, the given time nanoseconds have
     * elapsed, or if absolute, the given deadline in milliseconds
     * since Epoch has passed, or spuriously (i.e., returning for no
     * "reason"). Note: This operation is in the Unsafe class only
     * because <tt>unpark</tt> is, so it would be strange to place it
     * elsewhere.
     */
    public native void park(boolean isAbsolute, long time);

根據上面的原始碼分析,我們可以畫出AQS獨佔式獲取同步狀態的流程(即acquire方法的流程)


1593035-8400a3d403074d39.png
圖 2-1

2.AQS的release方法分析

public final boolean release(int arg) {
//tryRelease方法修改同步變數,直至state為0表明現在沒有執行緒佔有同步狀態        
if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
//喚醒頭結點的後繼節點
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;
        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) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);//喚醒後繼節點執行緒
    }
public static void unpark(Thread thread) {
        if (thread != null)
            unsafe.unpark(thread);
    }

3.最後

通過對AQS的原始碼分析,我們發現,對於同步佇列,每一個節點之間是沒有感知的,每個執行緒在嘗試獲取同步狀態失敗後,都會走一遍獨佔式獲取同步狀態的流程(見圖2-1),包括加入佇列尾部,進入等待狀態,或者被前驅喚醒等,各個佇列節點的獨立工作,構成了多執行緒爭搶設定AQS同步狀態的場景,獲得AQS的同步狀態就代表著執行緒獲取了鎖,這就是AQS的終極奧祕。。。

相關文章