1. 簡介
任意一個Java對象,都擁有一組監視器方法(定義在java.lang.Object上),主要包括wait()、
wait(long timeout)、notify()以及notifyAll()方法,這些方法與synchronized同步關鍵字配合,可以實現等待/通知模式。Condition接口也提供了類似Object的監視器方法,與Lock配合可以實現等待/通知模式,但是這兩者在使用方式以及功能特性上還是有差別的。
Condition定義了等待/通知兩種類型的方法,當前線程調用這些方法時,需要提前獲取到Condition對象關聯的鎖。Condition對象是由Lock對象(調用Lock對象的newCondition()方法)創建出來的,換句話說,Condition是依賴Lock對象的,而且在AQS中我們也提到AQS(同步器) 中使用Condition存放等待線程。
2. Condition使用
還是老規矩,寫事例前先看下Condition接口源碼爲我們定義了哪些api。
public interface Condition {
//當前線程進入等待狀態直到被通知(signal)或中斷。
//當前線程將進入運行狀態且從 awaitO 方法返回的情況,包括:
//其他線程調用該 Condition 的 signl() 或 signalAll()方法,而當前線程被選中喚醒
//其他線程(調用 interrupt(方法)中斷當前線程
//如果當前等待線程從 await()方法返回,那麼表明該線程已經獲取了 Condition對象所對應的鎖
void await() throws InterruptedException;
//當前線程進入等待狀態直到被通知,從方法名稱上可以看出該方法對中斷不敏感
void awaitUninterruptibly();
//當前線程進入等待狀態直到被通知、中斷或者超時。返回值表示剩餘的時間
//如果在 nanosTimeout納秒之前被喚醒,那麼返回值就是(nanosTimeout-實際耗時)
//如果返回值是0 或者負數,那麼可以認定已經超時了
long awaitNanos(long nanosTimeout) throws InterruptedException;
//指定等待時間,直到超時返回false
boolean await(long time, TimeUnit unit) throws InterruptedException;
//當前線程進入等待狀態直到被通知、中斷或者到某個時間。
//如果沒有到指定時間就被通知,方法返回true,否則,表示到了指定時間,方法返回 false
boolean awaitUntil(Date var1) throws InterruptedException;
//喚醒 一個等待在 Condition 上的線程,該線程從等待方法返回前必須獲得與Condition 相關聯的鎖
void signal();
//喚醒所有等待在Condition上的線程, 能夠從等待方法返回的線程必須獲得與Condition 相關聯的鎖
void signalAll();
}
Condition的使用方式比較簡單,需要注意在調用方法前獲取鎖,使用方式如下:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void conditionWait() throws InterruptedException {
lock.lock();
try {
condition.await();
} finally {
lock.unlock();
}
}
public void conditionSignal() throws InterruptedException {
lock.lock();
try {
condition.signal();
} finally {
lock.unlock();
}
}
如示例所示,一般都會將Condition對象作爲成員變量。當調用await()方法後,當前線程會釋放鎖並在此等待,而其他線程調用Condition對象的signal()方法,通知當前線程後,當前線程才從await()方法返回,並且在返回前已經獲取了鎖。
獲取一個Condition 必須通過Lock的 newCondition() 方法。下面通過一個有界隊列的示例來深入瞭解Condition的使用方式。有界隊列是一種特殊的隊列,當隊列爲空時,隊列的獲取操作將會阻塞獲取線程,直到隊列中有新增元素,當隊列已滿時,隊列的插入操作將會阻塞插入線程,直到隊列出現“空位”,其代碼如下:
public class BoundedQueue<T> {
private Object[] items;
// 添加的下標,刪除的下標和數組當前數量
private int addIndex, removeIndex, count;
private Lock lock = new ReentrantLock();
private Condition notEmpty = lock.newCondition();
private Condition notFull = lock.newCondition();
public BoundedQueue(int size) {
items = new Object[size];
}
// 添加一個元素,如果數組滿,則添加線程進入等待狀態,直到有"空位"
public void add(T t) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[addIndex] = t;
if (++addIndex == items.length)
addIndex = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
// 由頭部刪除一個元素,如果數組空,則刪除線程進入等待狀態,直到有新添加元素
public T remove() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[removeIndex];
if (++removeIndex == items.length)
removeIndex = 0;
--count;
notFull.signal();
return (T) x;
} finally {
lock.unlock();
}
}
}
上述示例中,BoundedQueue通過add(T t)方法添加一個元素,通過remove()方法移出一個元素。以添加方法爲例:
- 首先需要獲得鎖,目的是確保數組修改的可見性和排他性。
- 當數組數量等於數組長度時,表示數組已滿,則調用notFull.await(),當前線程隨之釋放鎖並進入等待狀態。
- 如果數組數量不等於數組長度,表示數組未滿,則添加元素到數組中,同時通知等待在notEmpty上的線程,數組中已經有新元素可以獲取。
在添加和刪除方法中使用while循環而非if判斷,目的是防止過早或意外的通知,只有條件符合才能夠退出循環。回想之前提到的等待/通知的經典範式,二者是非常類似的。
3. Condition實現分析
在 Condition newCondition() 方法中,返回一個 ConditionObject,ConditionObject是同步器AbstractQueuedSynchronizer的內部類,因爲Condition的操作需要獲取相關聯的鎖,所以作爲同步器的內部類也較爲合理。每個Condition對象都包含着一個隊列(以下稱爲等待隊列),該隊列是Condition對象實現等待/通知功能的關鍵。
下面將分析Condition的實現,主要包括:等待隊列、等待和通知,下面提到的Condition如果不加說明均指的是ConditionObject。
3.1 等待隊列
等待隊列是一個FIFO的隊列,在隊列中的每個節點都包含了一個線程引用,該線程就是在Condition對象上等待的線程,如果一個線程調用了Condition.await()方法,那麼該線程將會釋放鎖、構造成節點加入等待隊列並進入等待狀態。事實上,節點的定義複用了同步器中節點的定義,也就是說,同步隊列和等待隊列中節點類型都是同步器的靜態內部類AbstractQueuedSynchronizer.Node。
一個Condition包含一個等待隊列,Condition擁有首節點(firstWaiter)和尾節點(lastWaiter)。
其源碼如下:
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
....
}
當前線程調用Condition.await()方法,將會以當前線程構造節點,並將節點從尾部加入等待隊列,等待隊列的基本結構如圖所示:
如圖所示,Condition擁有首尾節點的引用,而新增節點只需要將原有的尾節點nextWaiter指向它,並且更新尾節點即可。上述節點引用更新的過程並沒有使用CAS保證,原因在於調用await()方法的線程必定是獲取了鎖的線程,也就是說該過程是由鎖來保證線程安全的。
同時還有一點需要注意的是:我們可以多次調用lock.newCondition()方法創建多個condition對象,也就是一個lock可以持有多個等待隊列。而在之前在Object的監視器模型上,一個對象擁有一個同步隊列和等待隊列,而併發包中的Lock(更確切地說是同步器)擁有一個同步隊列和多個等待隊列,其對應關係如圖:
如圖所示,Condition的實現是同步器的內部類,因此每個Condition實例都能夠訪問同步器提供的方法,相當於每個Condition都擁有所屬同步器的引用。
3.2 等待
調用Condition的await()方法(或者以await開頭的方法),會使當前線程進入等待隊列並釋放鎖,同時線程狀態變爲等待狀態。當從await()方法返回時,當前線程一定獲取了Condition相關聯的鎖。
如果從隊列(同步隊列和等待隊列)的角度看await()方法,當調用await()方法時,相當於同步隊列的首節點(獲取了鎖的節點)移動到Condition的等待隊列中。
Condition的await()方法,源碼如下所示:
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 當前線程加入等待隊列
Node node = addConditionWaiter();
// 釋放同步狀態,也就是釋放鎖
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
調用該方法的線程成功獲取了鎖的線程,也就是同步隊列中的首節點,該方法會將當前線程構造成節點並加入等待隊列中,然後釋放同步狀態,喚醒同步隊列中的後繼節點,然後當前線程會進入等待狀態。
當等待隊列中的節點被喚醒,則喚醒節點的線程開始嘗試獲取同步狀態。如果不是通過其他線程調用Condition.signal()方法喚醒,而是對等待線程進行中斷,則會拋出InterruptedException。
如果從隊列的角度去看,當前線程加入Condition的等待隊列,該過程如圖
如圖所示,同步隊列的首節點並不會直接加入等待隊列,而是通過**addConditionWaiter()**方法把當前線程構造成一個新的節點並將其加入等待隊列中。
3.3 通知
調用Condition的**signal()**方法,將會喚醒在等待隊列中等待時間最長的節點(首節點),在喚醒節點之前,會將節點移到同步隊列中。
Condition的signal()方法,如下:
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
調用該方法的前置條件是當前線程必須獲取了鎖,可以看到signal()方法進行了isHeldExclusively()檢查,也就是當前線程必須是獲取了鎖的線程。接着獲取等待隊列的首節點,將其移動到同步隊列並使用LockSupport喚醒節點中的線程。
節點從等待隊列移動到同步隊列的過程如圖所示
通過調用同步器的 enq(Node node) 方法,等待隊列中的頭節點線程安全地移動到同步隊列。當節點移動到同步隊列後,當前線程再使用LockSupport喚醒該節點的線程。
被喚醒後的線程,將從await()方法中的while循環中退出(isOnSyncQueue(Node node)方法返回true,節點已經在同步隊列中),進而調用同步器的**acquireQueued()方法加入到獲取同步狀態的競爭中。
成功獲取同步狀態(或者說鎖)之後,被喚醒的線程將從先前調用的await()方法返回,此時該線程已經成功地獲取了鎖。
Condition的signalAll()**方法,相當於對等待隊列中的每個節點均執行一次signal()方法,效果就是將等待隊列中所有節點全部移動到同步隊列中,並喚醒每個節點的線程。
4. 總結
- Condition是配合Lock實現等待/喚起機制
- ConditionObject 是AQS內部類,相當於其是AQS實現的一部分。
- ConditionObject 也是鏈表結構,有 firstWaiter(首節點)和lastWaiter(尾節點)
- ConditionObject 使用的節點結構和AQS中同步隊列中是同一結構
- ConditionObject 在AQS語義上來看是等待隊列,通過await()方法從等待隊列移入到同步隊列,這樣纔能有機會執行完成代碼
- AQS中如果獲取同步狀態失敗,則會從同步隊列移入到等待隊列,只要直到其它節點調用signal()方法,節點被喚醒移入到同步隊列中。
其實只要清楚Condition是鏈表結構,是用來配合lock,實現等待喚醒功能,其次還要直到在AQS中,其是等待隊列,在其次就是等待隊列和同步隊列節點是怎麼切換的。
5. 補充一個例子
下面附一個小例子:
private static ReentrantLock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
private static volatile boolean flag = false;
public static void main(String[] args) {
Thread waiter = new Thread(new waiter());
waiter.start();
Thread signaler = new Thread(new signaler());
signaler.start();
}
static class waiter implements Runnable {
@Override
public void run() {
lock.lock();
try {
while (!flag) {
System.out.println(Thread.currentThread().getName() + "當前條件不滿足等待");
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "接收到通知條件滿足");
} finally {
lock.unlock();
}
}
}
static class signaler implements Runnable {
@Override
public void run() {
lock.lock();
try {
flag = true;
condition.signalAll();
} finally {
lock.unlock();
}
}
}
輸出結果:
Thread-0當前條件不滿足等待
Thread-0接收到通知,條件滿足
參考文獻 《Java併發編程的藝術》