Java併發包原始碼學習系列:ReentrantLock可重入獨佔鎖詳解

天喬巴夏丶發表於2021-01-11

系列傳送門:

基本用法介紹

ReentrantLock位於java.util.concurrent(J.U.C)包下,是Lock介面的實現類。基本用法與synchronized相似,都具備可重入互斥的特性,但擁有更強大的且靈活的鎖機制。本篇主要從原始碼角度解析ReentrantLock,一些基本的概念以及Lock介面可以戳這篇:Java併發讀書筆記:Lock與ReentrantLock

ReentrantLock推薦用法如下:

class X {
    //定義鎖物件
    private final ReentrantLock lock = new ReentrantLock();
    // ...
    //定義需要保證執行緒安全的方法
    public void m() {
        //加鎖
        lock.lock();  
        try{
        // 保證執行緒安全的程式碼
        }
        // 使用finally塊保證釋放鎖
        finally {
            lock.unlock()
        }
    }
}

繼承體系

  • 實現Lock介面,提供了鎖的關鍵方法,如lock、unlock、tryLock等等,以及newCondition給lock關聯條件物件的方法。
  • 內部維護了一個Sync,它繼承AQS,實現AQS提供的獨佔式的獲取與釋放同步資源的方法,提供了可重入的具體實現。
  • Sync有兩個實現類,是公平鎖和非公平鎖的兩種實現,FairSync與NonfairSync。

獨佔鎖表示:同時只能有一個執行緒可以獲取該鎖,其他獲取該鎖的執行緒會被阻塞而被放入該所的AQS阻塞佇列裡面。這部分可以檢視:Java併發包原始碼學習系列:AQS共享式與獨佔式獲取與釋放資源的區別

構造方法

Sync直接繼承自AQS,NonfairSync和FairSync繼承了Sync,實現了獲取鎖的公平與非公平策略。

ReentrantLock中的操作都是委託給Sync物件來實際操作的。

    /** Synchronizer providing all implementation mechanics */
    private final Sync sync;

預設是使用非公平鎖:NonfairSync,可以傳入引數來指定是否使用公平鎖。

	// 預設使用的是 非公平的策略
    public ReentrantLock() {
        sync = new NonfairSync();
    }
	// 通過fair引數指定 策略
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

state狀態表示

在ReentrantLock中,AQS的state狀態值表示執行緒獲取該鎖的可重入次數,在預設情況下:

  • state值為0時表示當前鎖沒有被任何執行緒持有。
  • 當第一個執行緒第一次獲取該鎖時會嘗試使用CAS設定state的值為1,如果CAS成功則當前執行緒獲取了該鎖,然後記錄該鎖的持有者為當前執行緒
  • 在該執行緒沒有釋放鎖的情況下第二次獲取該鎖後,狀態值設定為2,為可重入次數。
  • 在該執行緒釋放鎖時,會嘗試使用CAS讓state值減1,如果減1後狀態值為0,則當前執行緒釋放該鎖

獲取鎖

void lock()方法

ReentrantLock的lock()方法委託給了sync類,根據建立sync的具體實現決定具體的邏輯:

NonfairSync

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            // CAS 設定獲取state值
            if (compareAndSetState(0, 1))
                // 將當前執行緒設定為鎖的持有者
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 設定失敗, 呼叫AQS的acquire方法
                acquire(1);
        }

state值的初始狀態為0,也就是說,第一個執行緒的CAS操作會成功將0設定為1,表示當前執行緒獲取到了鎖,然後通過setExclusiveOwnerThread方法將當前執行緒設定為鎖的持有者。

如果這時,其他執行緒也試圖獲取該鎖,則CAS失敗,走到acquire的邏輯。

    // AQS#acquire
	public final void acquire(int arg) {
        // 呼叫ReentrantLock重寫的tryAcquire方法
        if (!tryAcquire(arg) &&
            // tryAcquire方法返回false,則把當前執行緒放入AQS阻塞佇列中
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

欸,這個時候我們應該就有感覺了,我們之前在分析AQS的核心方法的時候說到過,AQS是基於模板模式設計的,下面的tryAcquire方法就是留給子類實現的,而NonfairSync中是這樣實現的:

        //NonfairSync#tryAcquire
        protected final boolean tryAcquire(int acquires) {
    		// 呼叫
            return nonfairTryAcquire(acquires);
        }

		final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            // 獲取當前狀態值
            int c = getState();
            // 如果當前狀態值為0,如果為0表示當前鎖空閒
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 看看當前的執行緒是不是鎖的持有者
            else if (current == getExclusiveOwnerThread()) {
                // 如果是的話 將狀態設定為  c + acquires
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

還是很好理解的哈,先看看鎖的狀態值是啥?

  • 如果是0,就CAS嘗試獲取鎖,將狀態從0變到1,並且設定鎖的持有者為當前執行緒,和之前的邏輯一樣啦。
  • 如果不是0,表示已經被某個執行緒持有啦,看看持有鎖的人是誰呢?如果是自己,那麼好辦,重入唄,將state變為nextc【原先state + 傳入的acquires】,返回true。這裡要注意:nextc<0表示可重入次數溢位。
  • 鎖已經被別人霸佔了,那就返回false咯,等待後續acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法,被置入AQS阻塞佇列中。

這裡非公平體現在獲取鎖的時候,沒有檢視當前AQS佇列中是否有比自己更早請求該鎖的執行緒存在,而是採取了搶奪策略

FairSync

公平鎖的tryAcquire實現如下:

        //FairSync#tryAcquire
		protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                // 狀態值為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;
        }

對比一下兩種策略,不必說,hasQueuedPredecessors方法一定是實現公平性的核心,我們來瞅瞅:

    // 如果當前執行緒有前驅節點就返回true。
	public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

該方法:如果當前執行緒有前驅節點就返回true,那麼我們想,不是前驅節點的情況有哪些呢?

  1. 佇列為空
  2. 佇列不為空,但當前執行緒節點是AQS的第一個節點。

知道這些之後,我們就明白最後那串表示式是什麼意思了:佇列裡面的第一個元素不是當前執行緒,返回true,說明在你之前還有人排著隊呢,你先別搶,先到先得

公平與非公平策略的差異

我們稍微總結一下:

Reentrant類的建構函式接受一個可選的公平性引數fair。這時候就出現兩種選擇:

  • 公平的(fair == true):保證等待時間最長的執行緒優先獲取鎖,其實就是先入隊的先得鎖,即FIFO。
  • 非公平的(fair == false):此鎖不保證任何特定的訪問順序。

公平鎖往往體現出的總體吞吐量比非公平鎖要低,也就是更慢,因為每次都需要看看佇列裡面有沒有在排隊的嘛。鎖的公平性並不保證執行緒排程的公平性,但公平鎖能夠減少"飢餓"發生的概率。

需要注意的是:不定時的tryLock()方法不支援公平性設定。如果鎖可用,即使其他執行緒等待時間比它長,它也會成功獲得鎖。

void lockInterruptibly()

該方法與lock方法類似,不同點在於,它能對中斷進行相應:當前執行緒在呼叫該方法時,如果其他執行緒呼叫了當前執行緒的interrupt()方法,當前執行緒會丟擲InterruptedException異常,然後返回。

    // ReentrantLock#lockInterruptibly
	public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
	// AQS#acquireInterruptibly
    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        // 如果當前執行緒被中斷,則直接丟擲異常
        if (Thread.interrupted())
            throw new InterruptedException();
        // 嘗試獲取資源
        if (!tryAcquire(arg))
            // 呼叫AQS可被中斷的方法
            doAcquireInterruptibly(arg);
    }

boolean tryLock()方法

嘗試獲取鎖,如果當前該鎖沒有被其他執行緒持有,則當前執行緒獲取該鎖並返回true,否則返回false。

大致邏輯和非公平鎖lock方法類似,但該方法會直接返回獲取鎖的結果,無論true或者false,它不會阻塞。

    // ReentrantLock# tryLock
	public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }
	abstract static class Sync extends AbstractQueuedSynchronizer {
		// Sync#nonfairTryAcquire
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 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;
        }
    }
  • tryLock() 實現方法,在實現時,希望能快速的獲得是否能夠獲得到鎖,因此即使在設定為 fair = true ( 使用公平鎖 ),依然呼叫 Sync#nonfairTryAcquire(int acquires) 方法。
  • 如果真的希望 tryLock() 還是按照是否公平鎖的方式來,可以呼叫 #tryLock(0, TimeUnit) 方法來實現。

boolean tryLock(long timeout, TimeUnit unit)

    // ReentrantLock# tryLock
    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
	// AQS#tryAcquireNanos
    public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }

嘗試獲取鎖,如果獲取失敗會將當前執行緒掛起指定時間,時間到了之後當前執行緒被啟用,如果還是沒有獲取到鎖,就返回false。

另外,該方法會對中斷進行的響應,如果其他執行緒呼叫了當前執行緒的interrupt()方法,響應中斷,丟擲異常。

釋放鎖

void unlock()方法

    // ReentrantLock#unlock
	public void unlock() {
        sync.release(1);
    }
	//AQS# release
    public final boolean release(int arg) {
        // 子類實現tryRelease
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

	abstract static class Sync extends AbstractQueuedSynchronizer {
		// Sync#tryRelease
        protected final boolean tryRelease(int releases) {
            // 計算解鎖後的次數,預設減1
            int c = getState() - releases;
            // 如果想要解鎖的人不是當前的鎖持有者,直接拋異常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 可重入次數為0,清空鎖持有執行緒
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            // 可重入次數還沒到0,只需要改變一下下state就可
            setState(c);
            return free;
        }
    }

嘗試釋放鎖,如果當前執行緒持有該鎖,呼叫該方法預設會讓AQS的state減1。

如果減1之後,state為0,當前執行緒會釋放鎖。

如果當前執行緒不是鎖持有者而企圖呼叫該方法,則丟擲IllegalMonitorStateException異常。

Condition實現生產者消費者

Condition是用來代替傳統Object中的wait()和notify()實現執行緒間的協作,Condition的await()和signal()用於處理執行緒間協作更加安全與高效

Condition的使用必須在lock()與unlock()之間使用,且只能通過lock.newCondition()獲取,實現原理我們之後會專門進行學習。

public class BlockingQueue {

    final Object[] items; // 緩衝陣列
    final ReentrantLock lock = new ReentrantLock(); // 非公平獨佔鎖
    final Condition notFull = lock.newCondition(); // 未滿條件
    final Condition notEmpty = lock.newCondition(); // 未空條件
    private int putIdx; // 新增操作的指標
    private int takeIdx; // 獲取操作的指標
    private int count; // 佇列中元素個數

    public BlockingQueue(int capacity) {
        if(capacity < 0) throw new IllegalArgumentException();
        items = new Object[capacity];
    }

    // 插入
    public void put(Object item) throws InterruptedException {
        try {
            lock.lock(); // 上鎖
            while (items.length == count) { // 滿了
                notFull.await(); // 其他插入執行緒阻塞起來
            }
            enqueue(item); // 沒滿就可以入隊
        } finally {
            lock.unlock(); // 不要忘記解鎖
        }
    }
    private void enqueue(Object item) {
        items[putIdx] = item;
        if (++putIdx == items.length) putIdx = 0; 
        count++;
        notEmpty.signal(); // 叫醒獲取的執行緒
    }

    // 獲取
    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await();// 阻塞其他獲取執行緒
            }
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
    
    private Object dequeue() {
        Object x = items[takeIdx];
        items[takeIdx] = null;
        if (++takeIdx == items.length) takeIdx = 0;
        count--;
        notFull.signal(); // 叫醒其他的插入執行緒
        return x;
    }
}

其實上面就是ArrayBlockingQueue刪減版的部分實現,感興趣的小夥伴可以看看原始碼的實現,原始碼上面針對併發還做了更細節的處理。

總結

API層面的獨佔鎖:ReentrantLock是底層使用AQS實現的可重入的獨佔鎖,區別於synchronized原生語法層面實現鎖語義,ReetrantLock通過lock()unlock()兩個方法顯式地實現互斥鎖。

state與可重入:AQS的state為0表示當前鎖空閒,大於0表示該鎖已經被佔用,某一時刻只有一個執行緒可以獲取該鎖。可重入性是通過判斷持鎖執行緒是不是當前執行緒,如果是,state+1,釋放鎖時,state-1,為0時表示徹底釋放。

公平與非公平策略:ReentrantLock擁有公平和非公平兩種策略,區別在於獲取鎖的時候是否會去檢查阻塞佇列中,是否存在當前執行緒的前驅節點,預設是非公平鎖策略。

豐富的鎖擴充套件:提供了響應中斷的獲取鎖方式lockInterruptibly,以及提供了快速響應的tryLock方法,及超時獲取等等方法。

condition:TODO一個ReentrantLock物件可以通過newCondition()同時繫結多個Condition物件,對執行緒的等待、喚醒操作更加詳細和靈活,這一點我們之後說到Condition的時候會再回過頭說的。

參考閱讀

相關文章