本系列文章經補充和完善,已修訂整理成書《Java程式設計的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連結
上節我們介紹了顯式鎖,本節介紹關聯的顯式條件,介紹其用法和原理。顯式條件也可以被稱做條件變數、條件佇列、或條件,後文我們可能會交替使用。
用法
基本概念和方法
鎖用於解決競態條件問題,條件是執行緒間的協作機制。顯式鎖與synchronzied相對應,而顯式條件與wait/notify相對應。wait/notify與synchronized配合使用,顯式條件與顯式鎖配合使用。
條件與鎖相關聯,建立條件變數需要通過顯式鎖,Lock介面定義了建立方法:
Condition newCondition();
複製程式碼
Condition表示條件變數,是一個介面,它的定義為:
public interface Condition {
void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();
}
複製程式碼
await()對應於Object的wait(),signal()對應於notify,signalAll()對應於notifyAll(),語義也是一樣的。
與Object的wait方法類似,await也有幾個限定等待時間的方法,但功能更多一些:
//等待時間是相對時間,如果由於等待超時返回,返回值為false,否則為true
boolean await(long time, TimeUnit unit) throws InterruptedException;
//等待時間也是相對時間,但引數單位是納秒,返回值是nanosTimeout減去實際等待的時間
long awaitNanos(long nanosTimeout) throws InterruptedException;
//等待時間是絕對時間,如果由於等待超時返回,返回值為false,否則為true
boolean awaitUntil(Date deadline) throws InterruptedException;
複製程式碼
這些await方法都是響應中斷的,如果發生了中斷,會丟擲InterruptedException,但中斷標誌位會被清空。Condition還定義了一個不響應中斷的等待方法:
void awaitUninterruptibly();
複製程式碼
該方法不會由於中斷結束,但當它返回時,如果等待過程中發生了中斷,中斷標誌位會被設定。
一般而言,與Object的wait方法一樣, 呼叫await方法前需要先獲取鎖,如果沒有鎖,會丟擲異常IllegalMonitorStateException。 await在進入等待佇列後,會釋放鎖,釋放CPU,當其他執行緒將它喚醒後,或等待超時後,或發生中斷異常後,它都需要重新獲取鎖,獲取鎖後,才會從await方法中退出。
另外,與Object的wait方法一樣,await返回後,不代表其等待的條件就一定滿足了,通常要將await的呼叫放到一個迴圈內,只有條件滿足後才退出。
一般而言,signal/signalAll與notify/notifyAll一樣,呼叫它們需要先獲取鎖,如果沒有鎖,會丟擲異常IllegalMonitorStateException。signal與notify一樣,挑選一個執行緒進行喚醒,signalAll與notifyAll一樣,喚醒所有等待的執行緒,但這些執行緒被喚醒後都需要重新競爭鎖,獲取鎖後才會從await呼叫中返回。
用法示例
ReentrantLock實現了newCondition方法,通過它,我們來看下條件的基本用法。我們實現與67節類似的例子WaitThread,一個執行緒啟動後,在執行一項操作前,等待主執行緒給它指令,收到指令後才執行,示例程式碼為:
public class WaitThread extends Thread {
private volatile boolean fire = false;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
@Override
public void run() {
try {
lock.lock();
try {
while (!fire) {
condition.await();
}
} finally {
lock.unlock();
}
System.out.println("fired");
} catch (InterruptedException e) {
Thread.interrupted();
}
}
public void fire() {
lock.lock();
try {
this.fire = true;
condition.signal();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
WaitThread waitThread = new WaitThread();
waitThread.start();
Thread.sleep(1000);
System.out.println("fire");
waitThread.fire();
}
}
複製程式碼
需要特別注意的是, 不要將signal/signalAll與notify/notifyAll混淆,notify/notifyAll是Object中定義的方法,Condition物件也有,稍不注意就會誤用,比如,對上面例子中的fire方法,可能會寫為:
public void fire() {
lock.lock();
try {
this.fire = true;
condition.notify();
} finally {
lock.unlock();
}
}
複製程式碼
寫成這樣,編譯器不會報錯,但執行時會丟擲IllegalMonitorStateException,因為notify的呼叫不在synchronized語句內。
同樣,避免將鎖與synchronzied混用,那樣非常令人混淆,比如:
public void fire() {
synchronized(lock){
this.fire = true;
condition.signal();
}
}
複製程式碼
記住,顯式條件與顯式鎖配合,wait/notify與synchronized配合。
生產者/消費者模式
在67節,我們用wait/notify實現了生產者/消費者模式,我們提到了wait/notify的一個侷限,它只能有一個條件等待佇列,分析等待條件也很複雜。在生產者/消費者模式中,其實有兩個條件,一個與佇列滿有關,一個與佇列空有關。使用顯式鎖,可以建立多個條件等待佇列。下面,我們用顯式鎖/條件重新實現下其中的阻塞佇列,程式碼為:
static class MyBlockingQueue<E> {
private Queue<E> queue = null;
private int limit;
private Lock lock = new ReentrantLock();
private Condition notFull = lock.newCondition();
private Condition notEmpty = lock.newCondition();
public MyBlockingQueue(int limit) {
this.limit = limit;
queue = new ArrayDeque<>(limit);
}
public void put(E e) throws InterruptedException {
lock.lockInterruptibly();
try{
while (queue.size() == limit) {
notFull.await();
}
queue.add(e);
notEmpty.signal();
}finally{
lock.unlock();
}
}
public E take() throws InterruptedException {
lock.lockInterruptibly();
try{
while (queue.isEmpty()) {
notEmpty.await();
}
E e = queue.poll();
notFull.signal();
return e;
}finally{
lock.unlock();
}
}
}
複製程式碼
定義了兩個等待條件:不滿(notFull)、不空(notEmpty),在put方法中,如果佇列滿,則在noFull上等待,在take方法中,如果佇列空,則在notEmpty上等待,put操作後通知notEmpty,take操作後通知notFull。
這樣,程式碼更為清晰易讀,同時避免了不必要的喚醒和檢查,提高了效率。Java併發包中的類ArrayBlockingQueue就採用了類似的方式實現。
實現原理
ConditionObject
理解了顯式條件的概念和用法,我們來看下ReentrantLock是如何實現它的,其newCondition()的程式碼為:
public Condition newCondition() {
return sync.newCondition();
}
複製程式碼
sync是ReentrantLock的內部類物件,其newCondition()程式碼為:
final ConditionObject newCondition() {
return new ConditionObject();
}
複製程式碼
ConditionObject是AQS中定義的一個內部類,不瞭解AQS請參看上節。ConditionObject的實現也比較複雜,我們通過一些主要程式碼來簡要探討其實現原理。ConditionObject內部也有一個佇列,表示條件等待佇列,其成員宣告為:
//條件佇列的頭節點
private transient Node firstWaiter;
//條件佇列的尾節點
private transient Node lastWaiter;
複製程式碼
ConditionObject是AQS的成員內部類,它可以直接訪問AQS中的資料,比如AQS中定義的鎖等待佇列。
我們看下幾個方法的實現,先看await方法。
await實現分析
下面是await方法的程式碼,我們通過新增註釋解釋其基本思路。
public final void await() throws InterruptedException {
// 如果等待前中斷標誌位已被設定,直接拋異常
if (Thread.interrupted())
throw new InterruptedException();
// 1.為當前執行緒建立節點,加入條件等待佇列
Node node = addConditionWaiter();
// 2.釋放持有的鎖
int savedState = fullyRelease(node);
int interruptMode = 0;
// 3.放棄CPU,進行等待,直到被中斷或isOnSyncQueue變為true
// isOnSyncQueue為true表示節點被其他執行緒從條件等待佇列
// 移到了外部的鎖等待佇列,等待的條件已滿足
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 4.重新獲取鎖
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
// 5.處理中斷,丟擲異常或設定中斷標誌位
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
複製程式碼
awaitNanos實現分析
awaitNanos與await的實現是基本類似的,區別主要是會限定等待的時間,如下所示:
public final long awaitNanos(long nanosTimeout) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
long lastTime = System.nanoTime();
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
if (nanosTimeout <= 0L) {
//等待超時,將節點從條件等待佇列移到外部的鎖等待佇列
transferAfterCancelledWait(node);
break;
}
//限定等待的最長時間
LockSupport.parkNanos(this, nanosTimeout);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
long now = System.nanoTime();
//計算下次等待的最長時間
nanosTimeout -= now - lastTime;
lastTime = now;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
return nanosTimeout - (System.nanoTime() - lastTime);
}
複製程式碼
signal實現分析
signal方法程式碼為:
public final void signal() {
//驗證當前執行緒持有鎖
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//呼叫doSignal喚醒等待佇列中第一個執行緒
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
複製程式碼
doSignal的程式碼就不列舉了,其基本邏輯是:
- 將節點從條件等待佇列移到鎖等待佇列
- 呼叫LockSupport.unpark將執行緒喚醒
小結
本節介紹了顯式條件的用法和實現原理。它與顯式鎖配合使用,與wait/notify相比,可以支援多個條件佇列,程式碼更為易讀,效率更高,使用時注意不要將signal/signalAll誤寫為notify/notifyAll。
從70節到本節,我們介紹了Java併發包的基礎 - 原子變數和CAS、顯式鎖和條件,基於這些,Java併發包還提供了很多更為易用的高層資料結構、工具和服務,從下一節開始,我們先探討一些併發資料結構。
(與其他章節一樣,本節所有程式碼位於 github.com/swiftma/pro…)
未完待續,檢視最新文章,敬請關注微信公眾號“老馬說程式設計”(掃描下方二維碼),深入淺出,老馬和你一起探索Java程式設計及計算機技術的本質。用心原創,保留所有版權。