1. 程式人生 > >Reentrant 可重入解釋

Reentrant 可重入解釋

原子操作 con link 歸納 基礎 gets 角色 jdk 無鎖

作者:知乎用戶
鏈接:https://www.zhihu.com/question/37168009/answer/88086943
來源:知乎
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請註明出處。

我們來看看問題,按照現在我看到的情況,題幹是:“怎樣證明synchronized鎖,Lock鎖是可重入的”,外加一個Java的標簽。

Java中,Synchronized確實是可重入的。另外Lock鎖這個定義並不準確,在Java中Lock只是一個接口,並且在doc中並沒有說明實現類一定是需要具備可重入的特性。Lock的實現眾多,其中最常見也是最為任何Java程序員熟知的是ReentrantLock。但是註意,不一定Lock的子類就是可重入的,例如netty中就有一個比較有趣的NoReentrantLock的實現。

那麽下面內容就以題目是Synchronized和ReentrantLock為前提進行。

我們第一步要明確什麽是“可重入的”。其對應的英文單詞是:Reentrant,哦不對,其實準確的說應該是“Re-entrant”。wikipedia有一個Reentrancy(computing)的解釋。不過在ReentrantLock的doc中找到這段話:
A ReentrantLock is owned by the thread last successfully locking, but not yet unlocking it. A thread invoking lock will return, successfully acquiring the lock, when the lock is not owned by another thread. The method will return immediately if the current thread already owns the lock.

最後一句話尤其重要,如果當前占用這個Reentrant的人就是當前線程,那麽就會立即返回。換成大白話說就是,一個線程獲取到鎖之後可以無限次地進入該臨界區 (通過調用lock.lock())。當然同樣也需要等同次數的unlock操作(這句話是我加的

OK,既然我們已經明白了Reentrant的含義。那麽如何證明呢?寫個程序是最簡單的辦法,一個線程遞歸的調用一個需要加鎖的函數(不要遞歸太深),看會不會hog住線程。這都是很好很好的,可我偏偏不喜歡,引自《白馬嘯西風》。我還是更傾向於learn java in the hardest way。

先,簡單介紹一下普通的lock的實現原理,這裏只介紹加鎖部分,下面是偽碼形式:
public void lock() {
  // step 1. try to change a atomic state
  boolean ok = state.compareAndSet(0, 1);
  
  // step 2. set exclusive thread if ok
  if (ok) {
    setExclusiveThread(Thread.current()); // 這只是個標誌位,不用太介意
    return;
  }

  // step 3. enqueue
  enqueue();

  // step 4. block
  Unsafe.park();

  // step 5. retry 
  lock();
}

小朋友們不要輕易模仿。沒有誰用這種傻逼的遞歸寫法的,除了我。完整的代碼比這個復雜,除了基本的流程,還要處理是否是公平鎖,處理線程中斷,以及一系列的無鎖數據結構等等。

幾個要點:
  • 通過一個原子狀態來控制誰進入臨界區
  • 通過一個鏈表隊列,記錄等待獲取鎖的線程
  • 通過Unsafe的park()函數,來把當前線程的運行狀態設置成掛起,並且停止調度
  • 當已經獲取鎖的線程調用unlock()函數的時候,就會使用Unsafe.unpark()函數來喚醒等待隊列頭部的線程
  • 喚醒之後,線程繼續試著獲取鎖,失敗則遞歸,成功則返回

慢著,知道上面的東西,離我們證明題幹還有一定的距離,繼續看。
Tips: 整個concurrent包源自於JSR-166,其作者就是大名鼎鼎的Doug Lea,說他是這個世界上對Java影響力最大的個人,一點也不為過。因為兩次Java歷史上的大變革,他都間接或直接的扮演了舉足輕重的角色。一次是由JDK 1.1到JDK 1.2,JDK1.2很重要的一項新創舉就是Collections,其Collections的概念可以說承襲自Doug Lea於1995年發布的第一個被廣泛應用的collections;一次是2004年所推出的Tiger。Tiger廣納了15項JSRs(Java Specification Requests)的語法及標準,其中一項便是JSR-166
就是這個小朋友,歸納總結出,嗯各種同步手段底層都需要一些共同的東西,所以寫了一個類叫java.util.concurrent.locks.AbstractQueuedSynchronizer。後來被簡稱為AQS框架,該框架將加鎖的步驟模板化了之後,提供了基本的列表、狀態控制等等手段。我們可以簡單看看lock的過程他是如何抽象的:
 public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
 }
一共四步:
  1. tryAcquire,抽象方法,由子類實現,子類通過控制原子變量來表示是否獲取鎖成功,類似於上文代碼的Step1、Step2
  2. addWaiter,已經實現的方法,表示將當前線程加入等待隊列,類似於上文的Step3
  3. acquireQueued(),掛起線程,喚醒後重試,類似於上文的Step4、Step5
  4. 處理線程中斷標誌位。

我們只需要記住一個重要的地方就是,子類只需要實現tryAcquire方法,就可以實現一個鎖,嗯,不錯!而這個tryAcquire方法最重要的就是利用AQS類中提供的原子操作來控制狀態。我們看一個最簡單的Mutex的例子:
 public boolean tryAcquire(int acquires) {
   assert acquires == 1; // Otherwise unused
   if (compareAndSetState(0, 1)) {
     setExclusiveOwnerThread(Thread.currentThread());
     return true;
   }
   return false;
 }

簡單解釋一下,compareAndSetState是父類AQS中提供的protected方法,setExclusiveOwnerThread同理。如此我們就實現了一個簡單的Mutex。

現在我們考慮一個問題,這個基於AQS實現的Mutex是不是可重入的呢?當然不是,線程A調用lock方法,然後就調用到這個tryAcquire函數中,顯然這個狀態就是被設置成了1。線程A第二次進來的時候,再次控制這個原子變量,發現就不好使了,就進入等待隊列。自己就被自己等死了。

好,最後就是重點,ReentrantLock也是在AQS的基礎上實現的,那麽我們來看,他的tryAcquire方法是怎麽寫的。簡單起見,ReentrantLock有公平和非公平的兩種實現,我們只關註可重入的特點,這裏就不介紹,我們直接看非公平的版本。
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;
}
我來解釋下這段代碼:
  • 如果當前的state(AQS提供的原子變量)=0,意味著沒有人占用,那麽我們compareAndSet來占用,並且設置自己為獨占線程
  • 如果獨占線程就是當前線程,那麽說明就是我自己鎖住啦(可重入),那麽把state計數累加。

貌似這樣就說通了。還有一個點就是不要小看這個累加哦,在unlock的時候也是一個累減的過程,也就是同一個線程針對同一個ReentrantLock對象調用了10次lock操作,那麽對應的,就需要調用10次unlock操作。才會真正的釋放lock。

我想差不多應該可以證明了吧..

對這個類比較感興趣的小朋友可以參考爸爸的兩篇博客:Java.concurrent.locks(1)-AQS、Java.concurrent.locks(2)-ReentrantLock。

然後現在已經晚上10點了,爸爸要回家睡覺了。同步塊的部分以後想起了再更吧。那不過是用c艹實現的版本,原理一致,代碼幾乎也差不多。

Reentrant 可重入解釋