ReentrantLock源碼解析——雖眾但寫

在看這篇文章時,筆者默認你已經看過AQS或者已經初步的了解AQS的內部過程。

  先簡單介紹一下ReentantLock,跟synchronized相同,是可重入的重量級鎖。但是其用法則相當不同,首先ReentrantLock顯式的調用lock方法表示接下來的這段程式碼已經被當前執行緒鎖住,其他執行緒需要執行時需要拿到這個鎖才能執行,而當前執行緒在執行完之後要顯式的釋放鎖,固定格式

lock.lock();  try {      doSomething();  } finally {      lock.unlock();  }  

1.ReentrantLock的demo程式

來通過下面這段程式碼簡單的了解ReentrantLock是如何使用的

	// 定義一個鎖  	private static Lock lock = new ReentrantLock();        /**       * ReentrantLock的使用例子,並且驗證其一些特性       * @param args 入參       * @throws Exception 錯誤       */      public static void main(String[] args) throws Exception {          // 執行緒池          ThreadPoolExecutor executor = ThreadPoolUtil.getInstance();            executor.execute(() -> {              System.err.println("執行緒1嘗試獲取lock鎖...");              lock.lock();              try {                  System.err.println("執行緒1拿到鎖並進入try,準備執行testForLock方法");                  // 調用下方的方法,驗證lock的可重入性                  testForLock();                  TimeUnit.MILLISECONDS.sleep(500);                  System.err.println("執行緒1try模組全部執行完畢,準備釋放lock鎖");              } catch (InterruptedException e) {                  e.printStackTrace();              } finally {                  lock.unlock();                  System.err.println("執行緒1釋放lock鎖,執行緒1釋放鎖2次,此時才算真正釋放,驗證了ReentrantLock加鎖多少次就要釋放多少次鎖");              }          });            // 先睡他100ms,保證執行緒1先拿到鎖          TimeUnit.MILLISECONDS.sleep(100);            executor.execute(() -> {              System.err.println("執行緒2嘗試獲取lock鎖...");              lock.lock();              try {                  System.err.println("執行緒2拿到鎖並進入try");              } finally {                  lock.unlock();                  System.err.println("執行緒2執行完畢,釋放lock鎖");              }          });        }        /**       * 驗證ReentrantLock具有可重入       */      public static void testForLock() throws InterruptedException {          System.err.println("執行緒1開始執行testForLock方法,正準備獲取lock鎖...");          lock.lock();          try {              System.err.println("testForLock成功獲取lock鎖,證明了ReentrantLock具有可重入性");              TimeUnit.MILLISECONDS.sleep(200);          } finally {              lock.unlock();              System.err.println("testForLock釋放lock鎖,執行緒1釋放鎖一次");          }      }  

結果圖:1585664568146

  從結果圖中,我們得到了很多資訊,比如ReentrantLock具備可重入性(testForLock方法得出),並且其釋放鎖的次數必須跟加鎖的次數保持一致(這樣才能保證正確性);此外ReentrantLock悲觀鎖,在某個執行緒獲取到鎖之後其他執行緒在其完全釋放之前不得獲取(執行緒2充分證明了這一點,其開始獲取鎖的時間要比執行緒1的執行時間快許多,但還是被阻塞住了)。

2.獲取鎖的方法——lock()

  okay,那來看下其內部是如何實現的,直接點擊lock()方法

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

看到其直接調用了synclock()方法,再點擊進入

abstract static class Sync extends AbstractQueuedSynchronizer {      // ...        abstract void lock();        // ...  }  

  可以看到Sync類是ReentrantLock的一個內部類,繼承了AQS框架,也就是說ReentrantLock就是AQS框架下的一個產物,那麼問題就變得簡單起來了。如果還沒了解過AQS的可以看下我另一篇文章——AQS框架詳解,看過之後再回頭看ReentrantLock,你會發現,就這?

  扯回來ReentrantLock,這邊可以看到內部類Sync是一個抽象類,lock()方法也是一個抽象方法,也就意味著這個lock會根據子類的不同實現執行不同操作,點開子類發現有兩個——公平鎖和非公平鎖

1585667693048

裡邊的具體實現先放一放,回到ReentrantLocklock方法

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

  直接調用說明sync已經被初始化過,那麼在哪裡進行初始化的呢?仔細翻一翻可以從ReentrantLock兩個構造方法中發現貓膩

/**   * 構造方法1   * 無參構造方法,直接將sync初始化為非公平鎖   */  public ReentrantLock() {      sync = new NonfairSync();  }    /**   * 構造方法2   * 帶參構造方法,根據傳進來的布爾值決定將sync初始化為公平還是非公平鎖   */  public ReentrantLock(boolean fair) {      sync = fair ? new FairSync() : new NonfairSync();  }  

  這裡順帶說一下,在AQS有一個同步隊列(CLH),是一種先進先出隊列。公平鎖的意思就是嚴格按照這個隊列的順序來獲取鎖,非公平鎖的意思就是不一定按照這個隊列的順序來。

  那現在知道sync是在創建ReentrantLock的時候就進行了初始化,我們就來看下公平和非公平鎖各自做了什麼吧。

2.1 非公平鎖

static final class NonfairSync extends Sync {      private static final long serialVersionUID = 7316153563782823691L;        final void lock() {          // 使用CAS嘗試將state改為1,如果成功了,則表示獲取鎖成功,設置當前執行緒為持有執行緒即可          if (compareAndSetState(0, 1))              setExclusiveOwnerThread(Thread.currentThread());          else              // 否則的話調用AQS的acquire方法乖乖入同步隊列等待去吧              acquire(1);      }        // AQS暴露出來需要子類重寫的方法      protected final boolean tryAcquire(int acquires) {          // 方法解釋在下方          return nonfairTryAcquire(acquires);      }  }    // 非公平鎖的tryAcquire方法,該方法是放在Sync抽象類中的,為了tryLock的時候使用  final boolean nonfairTryAcquire(int acquires) {      final Thread current = Thread.currentThread();      // 當前鎖的狀態      int c = getState();      // 如果是0則表示鎖是開放狀態,可以爭奪      if (c == 0) {          // 使用CAS設置為對應的值,在ReentrantLock中acquires的值一直是1          if (compareAndSetState(0, acquires)) {              // 成功了設置持有執行緒              setExclusiveOwnerThread(current);              return true;          }      }      /*       * 如果當前執行緒是持有執行緒,那麼state的值+1       * 這裡也是ReentrantLock可重入的原理       */      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;  }  

  非公平鎖基本的流程解釋在上方的程式碼中已經在注釋寫出,相信不難看懂。不過有個需要注意的點要說一下,首先要看清楚非公平鎖的定義,它是不一定按照隊列順序來獲取,不是不按照隊列順序獲取。

  從上面的程式碼我們也可以看出來,非公平鎖調用lock()方法的時候會先調用一次CAS來獲取鎖,成功了直接返回,這第一次操作沒有按照隊列的順序來,但也只有這一次。如果失敗了,入隊之後還是乖乖的得按照CLH同步隊列的順序來拿鎖,這一點要搞清楚。

2.3 公平鎖

static final class FairSync extends Sync {      private static final long serialVersionUID = -3000897897090466540L;        // lock方法直接調用AQS的acquire方法,連一點爭取的慾望都沒有      final void lock() {          acquire(1);      }        // 公平鎖的獲取資源方法,該方法是在acquire方法類調用的      protected final boolean tryAcquire(int acquires) {            // 整體邏輯還是挺簡單的,跟非公平有些類似          final Thread current = Thread.currentThread();          int c = getState();          if (c == 0) {              /*               * c==0表示當前鎖沒有被獲取               * 如果沒有前驅節點或者前驅節點是頭結點,               * 那麼使用CAS嘗試獲取資源               * 成功了設置持有執行緒並返回true,失敗了直接返回               */              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;      }  }  

  公平鎖的邏輯相對來說十分簡單,lock方法老老實實的去排隊獲取鎖,而獲取資源方法的邏輯也在程式碼注釋寫得很清楚了,沒有什麼需要多講的。

3.鎖釋放

上面的理解之後釋放鎖的邏輯就簡單的多了,直接放程式碼吧:

/*   * 解鎖方法直接調用AQS的release方法   * 而release方法的去向又是跟tryRelease的返回值直接相關   * tryRelease方法的實現在內部類Sync中,具體在下方   */  public void unlock() {      sync.release(1);  }    abstract static class Sync extends AbstractQueuedSynchronizer {      private static final long serialVersionUID = -5179523762034025860L;        // ...        // 釋放資源的方法      protected final boolean tryRelease(int releases) {          // 拿到當前鎖的加鎖次數          int c = getState() - releases;          // 當前執行緒必須是鎖持有執行緒才能操作          if (Thread.currentThread() != getExclusiveOwnerThread())              throw new IllegalMonitorStateException();          boolean free = false;          // 如果次數為0,表示完全釋放,清空持有執行緒          if (c == 0) {              free = true;              setExclusiveOwnerThread(null);          }          setState(c);          return free;      }     	// ...  }  

  釋放鎖的邏輯在注釋中解釋得很清楚了,看完也知道由於ReentrantLock是可重入的,所以鎖的數值會逐漸增加,那麼在釋放的時候也要一個一個逐一釋放

主要的邏輯還是AQSrelease方法中,這裡詳講的話篇幅太多,有興趣的話可以單獨看下AQS的文章,傳送門:AQS

4.ReentrantLock的可選擇性

  來講下ReentrantLockSynchonized的一大不同點之一——Condition。那麼condition是什麼呢,簡單來說就是將等待獲取資源的執行緒獨立出來分隊,什麼意思呢?舉個例子,現在有8個執行緒同時爭取一個鎖,我覺得太多了,就把這個8個執行緒平均分成4隊,等我覺得哪隊OK就將那一隊的執行緒叫出來爭取這個鎖。在這裡的condition就是隊伍,4隊就是4個condition

  另外說一句,condition(隊伍)中的執行緒是不參與鎖的競爭的,如果上方的8個執行緒我只將2個執行緒放入一個隊,其他執行緒不建立隊伍,那麼其他執行緒會參與鎖的競爭,而獨立到隊伍中的2個執行緒則不會,因為其被放在AQS等待隊列中,等待隊列是不參與資源的競爭的,我在另一篇文章——AQS框架詳解寫得很清楚了。還是那句話,AQS懂了再看ReentrantLock,理解難度就會低得多得多得多得多….

okay,那來簡單看下Condition如何使用

// 執行緒池  ThreadPoolExecutor executor = ThreadPoolUtil.getInstance();  // 這裡只建了一個condition起理解作用,自己有興趣的話可以多建幾個模擬多點場景  Condition condition = lock.newCondition();    executor.execute(() -> {      System.err.println("執行緒1嘗試獲取lock鎖...");      lock.lock();      try {          System.err.println("執行緒1拿到鎖並進入try");          System.err.println("執行緒1準備進行condition操作");          /*           * 將當前執行緒即執行緒1放入指定的這個condition中,           * 如果是其他condition則調用其他condition的await()方法           */          condition.await();          System.err.println("執行緒1結束condition操作");      } catch (InterruptedException e) {          e.printStackTrace();      } finally {          lock.unlock();          System.err.println("執行緒1執行完畢,釋放lock鎖");      }  });  // 保證執行緒1獲取鎖並且執行完畢  TimeUnit.MILLISECONDS.sleep(200);  executor.execute(() -> {      System.err.println("執行緒2嘗試獲取lock鎖...");      lock.lock();      try {          System.err.println("執行緒2拿到鎖並進入try");          // 喚醒condition的所有執行緒          condition.signalAll();          System.err.println("執行緒2將condition中的執行緒喚醒");      } finally {          lock.unlock();          System.err.println("執行緒2執行完畢,釋放lock鎖");      }  });  

結果圖:

1585749193417

可以從結果圖中看到,

  當執行緒調用了condition.await()的時候就被放入了condition中,並且此時將持有的鎖釋放,將自己掛起睡覺等待其他執行緒喚醒。所以執行緒2才能在執行緒1沒執行完的情況獲取到了鎖,並且執行緒2執行完操作之後將執行緒1喚醒,執行緒1此時其實是重新進入同步隊列(隊尾)爭取資源的,如果隊列前方還有執行緒在等待的話它是不會拿到的,要按照隊列順序獲取,可以自己在本地創多幾個執行緒試一下。

  通過這段簡單的程式碼之後明顯可以看到condition具有不錯的靈活性,也就是說提供了更多了選擇性,這也就是跟synchronized不同的地方,如果使用synchronized加鎖,那麼Object的喚醒方法只能喚醒全部,或者其中的一個,但是ReentrantLock不同,有了condition的幫助,可以不同的執行緒進行不同的分組,然後有選擇的喚醒其中的一組或者其中一組的隨機一個。

5.總結

  ReentrantLock的源碼如果有了AQS的基礎,那麼看起來是不費吹灰之力(開個玩笑,還是要比吹灰費勁的)。所以本章的篇幅也比較簡單,先從一個例子說明了ReentrantLock的用法, 並且通過這個例子介紹了ReentrantLock可重入、悲觀鎖的幾個特性;接著對其lock方法進行源碼跟蹤,從而了解到其內部的方法都是由繼承AQS的內部類Sync來實現的,而Sync又分成了兩個類,代表兩種不同的鎖——公平鎖和非公平鎖;接下來再講到兩種鎖的具體實現和釋放的邏輯,到這裡加鎖解鎖的流程就完整了;最後再介紹ReentrantLock的另一種特性——Condition,這種特性允許其選擇特定的執行緒來爭奪鎖,也可以選擇性的喚醒鎖,到這裡整篇文章就告一段落。

 

孤獨的人不一定是天才,還可能是得了鬱抑症。