面試中如何答好:ReentrantLock

張哥說技術發表於2023-10-11

來源:碼農本農

先了解一下

我們知道實現一把鎖要有如下幾個邏輯

  • 鎖的標識
  • 執行緒搶鎖的邏輯
  • 執行緒掛起的邏輯
  • 執行緒儲存邏輯
  • 執行緒釋放鎖的邏輯
  • 執行緒喚醒的邏輯

我們在講解AQS的時候說過AQS基本負責了實現鎖的全部邏輯,唯獨執行緒搶鎖和執行緒釋放鎖的邏輯是交給子類來實現了,而ReentrantLock作為最常用的獨佔鎖,其內部就是包含了AQS的子類實現了執行緒搶鎖和釋放鎖的邏輯。

我們在使用ReentrantLock的時候一般只會使用如下方法

 ReentrantLock lock=new ReentrantLock();
 lock.lock();
 lock.unlock();
 lock.tryLock();
 Condition condition=lock.newCondition();
 condition.await();
 condition.signal();
 condition.signalAll();     

技術架構

如果我們自己來實現一個鎖,那麼如何設計呢?

根據AQS的邏輯,我們寫一個子類sync,這個類一定會呼叫父類的acquire方法進行上鎖,同時重寫tryAcquire方法實現自己搶鎖邏輯,也一定會呼叫release方法進行解鎖,同時重寫tryRelease方法實現釋放鎖邏輯。

面試中如何答好:ReentrantLock

那麼ReentrantLock是怎麼實現的呢?

ReentrantLock的實現的類架構如下,ReentrantLock對外提供作為一把鎖應該具備的api,比如lock加鎖,unlock解鎖等等,而它內部真正的實現是透過靜態內部類sync實現,sync是AQS的子類,是真正的鎖,因為這把鎖需要支援公平和非公平的特性,所以sync又有兩個子類FairSync和NonfairSync分別實現公平鎖和非公平鎖。

面試中如何答好:ReentrantLock

因為是否公平說的是搶鎖的時候是否公平,那兩個子類就要在上鎖方法acquire的呼叫和搶鎖方法tryAcquire的重寫上做文章。

公平鎖做了什麼文章?

 static final class FairSync extends Sync {
  
        final void lock() {
            acquire(1);
        }

        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;
        }
    }

公平鎖比較簡單,直接呼叫了父級類AQS的acquire方法,因為AQS的鎖預設就是公平的排隊策略。

重寫tryAcquire方法的邏輯為:

  1. 判斷當前鎖是否被佔用,即state是否為0
  2. 如果當前鎖沒有被佔用,然後會判斷等待佇列中是否有執行緒在阻塞等待,如果有,那就終止搶鎖,如果沒有,就透過cas搶鎖,搶到鎖返回true,沒有搶到鎖返回false。
  3. 如果當前鎖已經被佔用,然後判斷佔用鎖的執行緒是不是自己,如果是,就會將state加1,表示重入,返回true。如果不是自己那就是代表沒有搶到鎖,返回false。

公平就公平在老老實實排隊。

非公平鎖做了什麼文章?

  static final class NonfairSync extends Sync {
      
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
    
    //nonfairTryAcquire程式碼在父類sync裡面
     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;
        }

非公平鎖也很簡單,沒有直接呼叫了父級類AQS的acquire方法,而是先透過cas搶鎖,它不管等待佇列中有沒有其他執行緒在排隊,直接搶鎖,這就體現了不公平。

它重寫tryAcquire方法的邏輯為:

  1. 判斷當前鎖是否被佔用,即state是否為0
  2. 如果當前鎖沒有被佔用,就直接透過cas搶鎖(不管等待佇列中有沒有執行緒在排隊),搶到鎖返回true,沒有搶到鎖返回false。
  3. 如果當前鎖已經被佔用,然後判斷佔用鎖的執行緒是不是自己,如果是,就會將state加1,表示重入,返回true。如果不是自己那就是代表沒有搶到鎖,返回false。

公平鎖和非公平分別重寫了tryAcquire方法,來滿足公平和非公平的特性。那麼tryAcquire方法也是需要子類重寫的,因為它和是否公平無關,因此tryAcquire方法被抽象到sync類中重寫。

sync類中
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;
        }

釋放鎖的邏輯如下:

  1. 獲取state的值,然後減1
  2. 如果state為0,代表鎖已經釋放,清空aqs中的持有鎖的執行緒欄位的值
  3. 如果state不為0,說明當前執行緒重入了,還需要再次釋放鎖
  4. 將state寫回

釋放鎖往往和搶鎖邏輯是對應的,每個子類搶鎖邏輯不同的話,釋放鎖的邏輯也會對應不同。

具體實現

接下來我們透過ReentrantLock的使用看下它的原始碼實現

class X {
            private final ReentrantLock lock = new ReentrantLock();
            Condition condition1=lock.newCondition();
            Condition condition2=lock.newCondition();
            public void m() {
                lock.lock();
                try {

                    if(條件1){
                        condition1.await();
                    }
                    if(條件2){
                        condition2.await();
                    }
                } catch (InterruptedException e) {

                } finally {
                    condition1.signal();
                    condition2.signal();
                    lock.unlock();
                }
            }
        }

先看這個方法:lock.lock()

ReentrantLock類
public void lock() {
        sync.lock();
    }
  NonfairSync 類中
  final void lock() {
    if (compareAndSetState(0, 1))
    setExclusiveOwnerThread(Thread.currentThread());
    else
      acquire(1);
  }
  FairSync 類中
  final void lock() {
      acquire(1);
  }     

公平鎖和非公平鎖中都實現了lock方法,公平鎖直接呼叫AQS的acquire,而非公平鎖先搶鎖,搶不到鎖再呼叫AQS的acquire方法進行上鎖

進入acquire方法後的邏輯我們就都知道了。

再看這個方法lock.unlock()

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

unlock方法內直接呼叫了AQS的Release方法進行解鎖的邏輯,進入release方法後邏輯我們都已經知道了,這裡不再往下跟。

最後看這個方法lock.tryLock()

  public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }

tryLock方法直接呼叫sync的tryAcquireNanos方法,看過AQS的應該知道tryAcquireNanos這個方法是父類AQS的方法,這個方法和AQS中的四個核心方法中的Acquire方法一樣都是上鎖的方法,無非是上鎖的那幾個步驟,呼叫tryAcquire方法嘗試搶鎖,搶不到鎖就會進入doAcquireNanos方法。

 public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }

doAcquireNanos這個方法做的其實就是入隊,阻塞等一系列上鎖操作,邏輯和Acquire方法中差不多,但是有兩點不同:

  1. 該方法支援阻塞指定時長。
  2. 該方法支援中斷拋異常。

看下下面的程式碼

  private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (nanosTimeout <= 0L)
            return false;
        final long deadline = System.nanoTime() + nanosTimeout;
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                nanosTimeout = deadline - System.nanoTime();
                if (nanosTimeout <= 0L)
                    return false;
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

這裡的阻塞不再是LockSupport類的park方法,而是parkNanos方法,這個方法支援指定時長的阻塞,AQS正是利用這個方法實現阻塞指定時長,當自動喚醒後,迴圈中會判斷是否超過設定時長,如果超過直接返回false跳出迴圈。

在阻塞期間,如果執行緒被中斷,就會丟擲異常,同樣會跳出迴圈,外面可以透過捕獲這個異常達到中斷阻塞的目的。

可見ReentrantLock其實啥也沒做,其tryLock方法完全是依賴AQS實現。

lock.newCondition();

在AQS那篇我們說過Condition是AQS中的條件佇列,可以按條件將一批執行緒由不可喚醒變為可喚醒。

ReentrantLock類
 public Condition newCondition() {
        return sync.newCondition();
 }
sync靜態內部類
final ConditionObject newCondition() {
            return new ConditionObject();
}

sync提供了建立Condition物件的方法,意味著ReentrantLock也擁有Condition的能力。

ReentrantLock和synchronized對比

我們下面說的ReentrantLock其實就是說AQS,因為它的同步實現主要在AQS裡面。

  1. 實現方面

    ReentrantLock是jdk級別實現的,其原始碼在jdk原始碼中可以檢視,沒有脫離java。

    synchronized是jvm級別實現的,synchronized只是java端的一個關鍵字,具體邏輯實現都在jvm中。

  2. 效能方面

    最佳化前的synchronized效能很差,主要表現在兩個方面:

    因為大多數情況下對於資源的爭奪並沒有那麼激烈,甚至於某個時刻可能只有一個執行緒在工作,在這種沒有競爭或者競爭壓力很小的情況下,如果每個執行緒都要進行使用者態到核心態的切換其實是很耗時的。

    jdk1.6對synchronized底層實現做了最佳化,最佳化後,在單執行緒以及併發不是很高的情況下透過無鎖偏向和自旋鎖的方式避免使用者態到核心態的切換,因此效能提高了,最佳化後的synchronized和ReentrantLock效能差不多了。

    ReentrantLock是在jdk實現的,它申請互斥量就是對鎖標識state的爭奪,它是透過cas方式實現。在java端實現。

    對於爭奪不到資源的執行緒依然要阻塞掛起,但凡阻塞掛起都要依賴於作業系統底層,這一步的使用者態到核心態的切換是避免不了的。

    因此在單執行緒進入程式碼塊的時候,效率是很高的,因此我們說ReentrantLock效能高於原始的synchronized

  • 申請互斥量

    synchronized的鎖其實就是爭奪Monitor鎖的擁有權,這個爭奪過程是透過作業系統底層的互斥原語Mutex實現的,這個過程會有使用者態到核心態的切換。

  • 執行緒阻塞掛起

    沒能搶到到Monitor鎖擁有權的執行緒要阻塞掛起,阻塞掛起這個動作也是依靠作業系統實現的,這個過程也需要使用者態到核心態的切換。

  • 特性方面

    兩個都是常用的典型的獨佔鎖。

    ReentrantLock可重入,可中斷,支援公平和非公平鎖,可嘗試獲取鎖,可以支援分組將執行緒由不可喚醒變為可喚醒。

    synchronized可重入,不可中斷,非公平鎖,不可嘗試獲取鎖,只支援一個或者全部執行緒由不可喚醒到可喚醒。

  • 使用方面

  • synchronized不需要手動釋放鎖,ReentrantLock需要手動釋放鎖,需要考慮異常對釋放鎖的影響避免異常導致執行緒一直持有鎖。

    以下是兩個鎖的使用方式

    class X {
                private final ReentrantLock lock = new ReentrantLock();
                Condition condition1=lock.newCondition();
                Condition condition2=lock.newCondition();
                public void m() {
                    lock.lock();
                    try {

                        if(1==2){
                            condition1.await();
                        }
                        if(1==3){
                            condition2.await();
                        }
                    } catch (InterruptedException e) {

                    } finally {
                        condition1.signal();
                        condition2.signal();
                        lock.unlock();
                    }
                }
            }
       class X {
                private final testtest sync=new testtest();;
                public void m() throws InterruptedException {
                    synchronized(sync){
                        if(1==2){
                            sync.wait();
                        }
                        sync.notify();
                        sync.notifyAll();
                    }
                }
            }

    對比程式碼及特性說明:

    1. 兩個鎖都是依賴一個物件:lock和sync

    2. condition和wait方法具有同樣的效果,進入condition和wait的執行緒將陷入等待(不可喚醒狀態),只有被分別呼叫signal和notify方法執行緒才會重新變為可喚醒狀態,請注意是可喚醒,而不是被喚醒。

    3. 可喚醒是說具備了競爭資源的資格,資源空閒後,synchronized中會在可喚醒狀態的執行緒中隨機挑選一個執行緒去拿鎖,而ReentrantLock中不可喚醒的執行緒變為可喚醒狀態,其實就是將條件佇列中的執行緒搬到等待佇列中排隊,只有隊頭的才會去嘗試拿鎖。

    4. ReentrantLock分批將執行緒由不可喚醒變為可喚醒也在這段程式碼中體現了,程式碼中按照不同的條件將執行緒放入不同的condition,每個condition就是一個組,釋放的時候也可以按照不同的條件進行釋放。而synchronized中進入wait的執行緒不能分組,釋放也只能隨機釋放一個或者全部釋放。


    來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024923/viewspace-2988034/,如需轉載,請註明出處,否則將追究法律責任。

    相關文章