Reentrant 可重入解釋

做者:知乎用戶
連接:https://www.zhihu.com/question/37168009/answer/88086943
來源:知乎
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

咱們來看看問題,按照如今我看到的狀況,題幹是:「怎樣證實synchronized鎖,Lock鎖是可重入的」,外加一個Java的標籤。html

Java中,Synchronized確實是可重入的。另外Lock鎖這個定義並不許確,在Java中Lock只是一個接口,而且在doc中並無說明實現類必定是須要具有可重入的特性。Lock的實現衆多,其中最多見也是最爲任何Java程序員熟知的是ReentrantLock。可是注意,不必定Lock的子類就是可重入的,例如netty中就有一個比較有趣的NoReentrantLock的實現。java

那麼下面內容就以題目是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,抽象方法,由子類實現,子類經過控制原子變量來表示是否獲取鎖成功,相似於上文代碼的Step一、Step2
  2. addWaiter,已經實現的方法,表示將當前線程加入等待隊列,相似於上文的Step3
  3. acquireQueued(),掛起線程,喚醒後重試,相似於上文的Step四、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第二次進來的時候,再次控制這個原子變量,發現就很差使了,就進入等待隊列。本身就被本身等死了。ui

好,最後就是重點,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。atom

我想差很少應該能夠證實了吧..spa

對這個類比較感興趣的小朋友能夠參考爸爸的兩篇博客: Java.concurrent.locks(1)-AQSJava.concurrent.locks(2)-ReentrantLock
而後如今已經晚上10點了,爸爸要回家睡覺了。同步塊的部分之後想起了再更吧。那不過是用c艹實現的版本,原理一致,代碼幾乎也差很少。
相關文章
相關標籤/搜索