文章目錄
-
-
- 1. 概述
- 2. 初識Condition
- 3. 5種await方法的使用
-
- 3.1 await()
- 3.2 awaitUninterruptibly()
- 3.3 awaitNanos(long nanosTimeout)
- 3.4 awaitUntil(Date deadline)
- 3.5 await(long time, TimeUnit unit)
- 4. 5種await方法的源碼分析
-
- 4.1 await()
- 4.2 awaitUninterruptibly()
- 4.3 awaitNanos(long nanosTimeout)
- 4.4 awaitUntil(Date deadline)
- 4.5 await(long time, TimeUnit unit)
- 5. signal和signalAll的源碼分析
-
1. 概述
本文是分析ReentrantLock源碼的第三篇部落格,介紹Condition的使用和分析源碼的實作細節。之前的兩篇部落格連結如下,有興趣的讀者不妨看看。
- 【圖解】一篇搞定ReentrantLock的加鎖和解鎖過程
- ReentrantLock中4種加鎖方式的使用差別和源碼實作的細節差異
通過這篇部落格可以學習到Condition當中5種await方法,signal和signalAll方法的使用和源碼的實作細節。
2. 初識Condition
在介紹方法的使用和分析源碼之前,先來了解一下Condition是什麼。
可以把Condition看作是Object螢幕的替代品。衆所周知,Object有wait()和notify()方法,用于線程間的通信。并且這兩個方法隻能在synchronized同步塊内才可以調用,所有線程的等待和喚醒都需要關聯到螢幕對象的WaitSet集合。
Condition同樣可以實作上面的線程通信。不同點在于,synchronized鎖對象關聯的螢幕對象僅有一個,是以等待隊列也隻有一個。而一個ReentrantLock可以有多個Condition,這樣可以根據不同的業務需求,在使用同一個lock鎖對象的基礎上使用多個等待隊列,讓不同性質的線程加入到不同的等待隊列當中。
AQS當中Condition的實作類是ConditionObject,它是AQS的内部類,是以無法直接執行個體化。可以配合ReentrantLock來使用。
ReentrantLock中有newCondition()的方法,來執行個體化一個ConditionObject對象,是以可以調用多次newCondition()方法來得到多個等待隊列。
3. 5種await方法的使用
3.1 await()
先來看一個比較簡單的例子,一個線程,拿到鎖由于某些條件無法滿足,調用condition.await()方法
@Slf4j(topic = "s")
public class AwaitTest1 {
static ReentrantLock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) {
new Thread(() -> {
try {
lock.lock();
log.debug("因為某些條件無法滿足,進入等待");
condition.await();
log.debug("條件滿足了被喚醒,開始工作");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t1").start();
}
}
控制台輸出如下,僅僅列印出了一句話,說明調用await方法之後,該線程就不會繼續往下執行代碼了。這就和Object的wait方法很像,需要另一個線程調用notify來喚醒。不過此處的方法名字不叫做notify,而是signal

修改上面的測試代碼如下:
@Slf4j(topic = "s")
public class AwaitTest1 {
static ReentrantLock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
try {
lock.lock();
log.debug("因為某些條件無法滿足,進入等待");
condition.await();
log.debug("條件滿足了被喚醒,開始工作");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t1").start();
TimeUnit.SECONDS.sleep(4);
lock.lock();
condition.signal();
lock.unlock();
}
}
此時的結果如下,4秒後,主線程将t1線程喚醒,t1線程就繼續執行後面的邏輯啦,列印了開始工作。
上面的就是await/signal最基本的使用例子。由兩個線程來協作完成,一個線程等待,另一個線程負責喚醒。
3.2 awaitUninterruptibly()
這一節來看看awaitUninterruptibly()和await()方法有什麼樣的不同。
從名字上看,該方法多了Uninterruptibly,不可打斷的意思。那麼就寫一個測試代碼,打斷一下正在等待的線程看看有什麼差別。
先來試一下調用了await()方法的線程被打斷的結果,測試代碼如下:
@Slf4j(topic = "s")
public class AwaitTest2 {
static ReentrantLock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
lock.lock();
log.debug("因為某些條件無法滿足,進入等待");
condition.await();
log.debug("條件滿足了被喚醒,開始工作");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t1");
thread.start();
TimeUnit.SECONDS.sleep(4);
thread.interrupt();//打斷t1線程
}
}
控制台的輸出如下,打斷t1線程之後,t1線程會抛出中斷異常。
那麼調用awaitUninterruptibly()的結果呢?測試代碼如下,僅僅将await替換成awaitUninterruptibly。
@Slf4j(topic = "s")
public class AwaitTest2 {
static ReentrantLock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
lock.lock();
log.debug("因為某些條件無法滿足,進入等待");
condition.awaitUninterruptibly();//僅修改此處
log.debug("條件滿足了被喚醒,開始工作");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t1");
thread.start();
TimeUnit.SECONDS.sleep(4);
thread.interrupt();
}
}
控制台輸出如下,并不會抛出中斷異常。
總結
- 調用了await方法的線程會因為中斷抛出異常。
- 調用了awaitUninterruptibly方法的線程不會因為中斷抛出中斷異常。
底層是如何實作的呢?第4部分會分析,繼續往下看。
3.3 awaitNanos(long nanosTimeout)
上面的兩個方法在不發生異常的情況下,會一直在等待被其他線程喚醒。接下來的三個方法,都是帶有時間的等待,在一個時間範圍内等待,超過這個時間範圍,那麼就會自己醒來。
測試代碼如下:
@Slf4j(topic = "s")
public class AwaitTest3 {
static ReentrantLock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
lock.lock();
log.debug("因為某些條件無法滿足,進入等待");
condition.awaitNanos(5000000000l);//5秒
log.debug("條件滿足了被喚醒,或逾時,開始工作");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t2");
thread.start();
}
}
控制台輸出結果如下,等待5秒後,t2線程自己醒來了,繼續執行代碼。
參數的意思是一個納秒時間,截止的時間是目前時間+納秒時間。在截止時間之前,t2線程可以被其他線程叫醒(signal)或者中斷(抛出中斷異常)。如果超過截止時間,則t2線程自己醒來執行下面的代碼。
提問:超過截止時間,t2線程醒來後是立馬執行接下來的代碼嗎?
下面再寫一個測試例子看看結果如何:
@Slf4j(topic = "s")
public class AwaitTest3 {
static ReentrantLock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
// t2線程 因為某條件不滿足 進入等待隊列
Thread thread = new Thread(() -> {
try {
lock.lock();
log.debug("因為某些條件無法滿足,進入等待");
condition.awaitNanos(5000000000l);//5秒
log.debug("條件滿足了被喚醒,開始工作");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t2");
thread.start();
TimeUnit.MILLISECONDS.sleep(100);
lock.lock();
// 建立5個線程,因為拿不到鎖都進入阻塞隊列
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(() -> {
try {
log.debug("t" + (finalI + 3) + "線程拿不到鎖 進入阻塞隊列");
lock.lock();
log.debug("t" + (finalI + 3) + "線程拿到鎖,開始工作");
TimeUnit.SECONDS.sleep(2);//模拟工作時間2秒
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t" + (i + 3)).start();
}
TimeUnit.MILLISECONDS.sleep(100);//確定t3 - t7 5個線程都進入阻塞隊列
lock.unlock();
}
}
先說明一下代碼的意圖,首先t2線程因為不滿足某些條件而調用awaitNanos()方法進入等待隊列。之後主線程拿鎖,for循環建立5個線程,這5個線程由于拿不到鎖會進入阻塞隊列,至于為什麼,之前的部落格已經說明過了,這裡不再贅述。
我們來看看結果,看看t2線程是在什麼時候執行工作的。控制台輸出如下:
可以看到在10秒的時候,它進入了等待隊列,但是在20秒的時候,他才繼續工作。期間相差的這10秒中,恰好是5個線程,每個線程工作2秒的時間總和。
是以這裡可以猜想,t2醒來後它跑到了阻塞隊列當中,到底是不是這樣的呢?第4部分源碼分析的時候再證明。
3.4 awaitUntil(Date deadline)
該方法也是一個規定時間的等待,在截止時間之前,線程可以被其他線程叫醒(signal)或者中斷(抛出中斷異常)。如果超過截止時間,則線程自己醒來執行下面的代碼。
隻是這裡傳入的參數直接是一個截止時間,不再像上面一樣需要計算一個截止時間。
測試代碼如下:
@Slf4j(topic = "s")
public class AwaitTest4 {
static ReentrantLock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
// t1線程 因為某條件不滿足 進入等待隊列
Thread thread = new Thread(() -> {
try {
lock.lock();
log.debug("因為某些條件無法滿足,進入等待");
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND, 5);
condition.awaitUntil(calendar.getTime());//5秒
log.debug("條件滿足了被喚醒,或逾時,開始工作");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t1");
thread.start();
}
}
控制台輸出如下:
3.5 await(long time, TimeUnit unit)
該方法傳入等待的時間,和時間機關,相比較于awaitNanos(long nanosTimeout)方法更加的靈活。時間和時間機關進行配合。計算一個截止時間,作用和上面兩個方法一樣。
測試代碼如下:
@Slf4j(topic = "s")
public class AwaitTest5 {
static ReentrantLock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
// t1線程 因為某條件不滿足 進入等待隊列
Thread thread = new Thread(() -> {
try {
lock.lock();
log.debug("因為某些條件無法滿足,進入等待");
condition.await(5, TimeUnit.SECONDS);//5秒
log.debug("條件滿足了被喚醒,或逾時,開始工作");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t1");
thread.start();
}
}
控制台輸出如下:
4. 5種await方法的源碼分析
通過第三部分的方法使用介紹,相信讀者已經掌握了這5種方法是如何使用的,以及使用的差別。下面來分析分析底層源碼是如何實作的。
4.1 await()
- 首先判斷是否中斷過,如果發生過中斷,那麼就會抛出異常。
- 調用addConditionWaiter方法将目前線程封裝成Node結點,并加入到等待隊列的末尾。
- addConditionWaiter的代碼如下 當隊尾結點不屬于等待狀态的時候,則調用unlinkCanceledWaiters()将不處于等待狀态的所有結點從等待隊列中移除,具體的代碼如下:
ReentrantLock Condition的使用和實作原理(不留死角!!!) ReentrantLock Condition的使用和實作原理(不留死角!!!)
- addConditionWaiter的代碼如下
- 接着調用fullyRelease釋放鎖,并記錄下狀态值。說明await()的線程不再持有鎖,這一點和Object中的wait方法是一樣的。
- fullyRelease代碼如下,立馬調用了release方法釋放鎖的過程,之前的部落格中介紹過,不再贅述。
ReentrantLock Condition的使用和實作原理(不留死角!!!)
- fullyRelease代碼如下,立馬調用了release方法釋放鎖的過程,之前的部落格中介紹過,不再贅述。
- 調用isOnSyncQueue判斷是否在同步隊列上,如果目前結點不在同步隊列上,說明他在等待隊列上,将其阻塞。這裡用while循環是為了,等它下次醒來之後再一次的判斷是否在同步隊列上,如果還是不在同步隊列,說明他還在等待隊列當中。它就需要繼續等待,将其阻塞。
- 等到他下次被喚醒了,會調用checkInterruptWhileWaiting,判斷在阻塞期間是否發生了中斷。如果發生了中斷,說明取消等待。代碼如下:
- 如果發生了中斷,那麼則會調用transferAfterCanceledWait方法
ReentrantLock Condition的使用和實作原理(不留死角!!!) -
transferAfterCanceledWait方法有兩種情況
第一種:在調用signal喚醒之前取消,那麼就可以cas成功,将結點的狀态更新,然後将其轉移到同步隊列,傳回true。
第二種:在調用signal之後發生了中斷,那麼就傳回false。此處的while循環是為了確定signal方法執行的時候将結點順利轉移到同步隊列。signal方法的執行邏輯後面會講。
ReentrantLock Condition的使用和實作原理(不留死角!!!) - 總結:在signal調用前發生了中斷,會傳回抛出異常的标記THROW_IE,在signal調用後發生了中斷會傳回重新中斷的标記REINTERRUPT。
- 如果發生了中斷,那麼則會調用transferAfterCanceledWait方法
- 再接着就是調用acquireQueued方法,此時的結點一定在同步隊列上了,是以該方法的執行邏輯,就和之前部落格中介紹過的同步阻塞的結點搶鎖的邏輯一樣。不清楚的讀者可以回看之前的部落格。
- 之後就是,判斷目前結點的nextWaiter是否為空,如果不為空的話,調用unlinkCancelledWaiters()将取消等待的結點從等待隊列中移除。
- 最後判斷interruptMode響應中斷的模式,如果不等于0的話,說明發生過中斷。要麼抛出異常,要麼重新中斷。
ReentrantLock Condition的使用和實作原理(不留死角!!!)
4.2 awaitUninterruptibly()
分析過await方法的源碼,再來看接下來的源碼就比較容易了。該方法與await的主要差別在于,不可中斷。是以如果發生了中斷,并不會抛出異常,隻有一個措施就是重新中斷(中斷補償)。下面來看看代碼:
大緻邏輯基本相同,僅僅在發生中斷的處理上不太一樣。此處不需要記錄響應中斷的模式,無論結點是在同步隊列還是等待隊列上發生的中斷,都采取中斷補償的機制。
4.3 awaitNanos(long nanosTimeout)
該方法讓線程在一個時間範圍内等待,超過這個時間範圍,那麼就會自己醒來。源碼實作如下:
根據傳入的參數,計算出等待的截止時間,邏輯和前面的都差不多。
最主要的差別,在于紅色框框的部分,while循環的條件是結點在等待隊列上。如果剩餘等待時間nanosTimeout小于等于0,那麼它就會取消等待,調用transferAfterCanelledWait方法進行隊列轉移,轉移到同步隊列上。
如果剩餘時間大于spinForTimeoutThreshold,那麼該線程會阻塞。這裡設定一個門檻值,是為了避免時間過短,導緻頻繁的系統調用(阻塞,喚醒)。
4.4 awaitUntil(Date deadline)
該方法和上面的幾乎一樣,隻是不需要計算截止時間,傳入的參數就是截止的時間。
4.5 await(long time, TimeUnit unit)
該方法可以說是awaitNanos(long time)的更新版吧,根據時間機關和傳入的數字,轉換成納秒時長。之後的邏輯都一樣的。
5. signal和signalAll的源碼分析
- signal源碼分析
ReentrantLock Condition的使用和實作原理(不留死角!!!) - 調用isHeldExclusively()判斷目前線程是否是鎖的持有者。如果不是的話,抛出異常。
ReentrantLock Condition的使用和實作原理(不留死角!!!) - 拿到等待隊列中的第一個結點,如果不為空則調用doSignal方法,實作喚醒線程。
ReentrantLock Condition的使用和實作原理(不留死角!!!) - 将頭結點從等待隊列中移除,更新隊列的頭結點。
- 讓嘗試轉移first結點。因為是多線程,是以可能執行到這裡的時候,first結點已經轉移了。那麼就要将first指針指向新的頭結點。嘗試喚醒新的頭結點。
- 這就是這do-while循環的意圖。
ReentrantLock Condition的使用和實作原理(不留死角!!!) - 嘗試CAS修改結點的狀态,如果失敗了傳回false。說明該結點已經轉移了。
- 如果修改成功。那麼就調用enq方法,将結點從等待隊列轉移到同步隊列,enq方法會傳回node結點的前驅。
-
然後判斷前驅結點p的ws。此處分2種情況。
第一種:ws大于0,說明這個結點是一個取消狀态的結點,那麼就可以喚醒目前node結點的線程。
第二種:ws<=0,CAS修改前驅結點的狀态為-1。如果修改失敗了,說明它本身結點的狀态就是-1了。那麼此時它有義務喚醒後繼結點,也就是喚醒目前node結點的額線程。
- 調用isHeldExclusively()判斷目前線程是否是鎖的持有者。如果不是的話,抛出異常。
- signalAll源碼分析 這裡和上面的邏輯是一樣的。隻是内部調用的方法不一樣,主要來看看doSignalAll方法的不同。
ReentrantLock Condition的使用和實作原理(不留死角!!!) 該方法的邏輯也很簡單,從頭結點開始,一個一個的将等待隊列當中的結點轉移到同步隊列當中。ReentrantLock Condition的使用和實作原理(不留死角!!!)