1. 簡介
在沒有Lock
之前,我們使用synchronized
來控制同步,配合Object
的wait()
、notify()
系列方法可以實現等待/通知模式。在JavaSE 5
後,Java
提供了Lock
接口,相對於Synchronized
而言,Lock
提供了條件Condition
,對線程的等待、喚醒操作更加詳細和靈活。
Object
的監視器方法與Condition
接口的對比:
Condition
提供了一系列的方法來對阻塞和喚醒線程:
1、await()
:造成當前線程在接到信號或被中斷之前一直處於等待狀態。
2、await(long time, TimeUnit unit)
:造成當前線程在接到信號、被中斷或到達指定等待時間之前一直處於等待狀態。
3、awaitNanos(long nanosTimeout)
:造成當前線程在接到信號、被中斷或到達指定等待時間之前一直 處於等待狀態。返回值表示剩餘時間,如果在nanosTimesout之前喚醒,那麼返回值 = nanosTimeout - 消耗時間,如果返回值 <= 0 ,則可以認定它已經超時了。
4、awaitUninterruptibly()
:造成當前線程在接到信號之前一直處於等待狀態。【注意:該方法對中斷不敏感】。
5、awaitUntil(Date deadline)
:造成當前線程在接到信號、被中斷或到達指定最後期限之前一直處於等待狀態。如果沒有到指定時間就被通知,則返回true
,否則表示到了指定時間,返回返回false
。
6、signal()
:喚醒一個等待線程。該線程從等待方法返回前必須獲得與Condition
相關的鎖。
7、signalAll()
:喚醒所有等待線程。能夠從等待方法返回的線程必須獲得與Condition
相關的鎖。
Condition
是一種廣義上的條件隊列。他爲線程提供了一種更爲靈活的等待/通知模式,線程在調用await
方法後執行掛起操作,直到線程等待的某個條件爲真時纔會被喚醒。Condition
必須要配合鎖一起使用,因爲對共享狀態變量的訪問發生在多線程環境下。一個Condition
的實例必須與一個Lock
綁定,因此Condition
一般都是作爲Lock
的內部實現。
2. Condition的圖解
在Object
的監視器模型上,一個對象擁有一個同步隊列和等待隊列。而在併發包中的Lock
(同步器)擁有一個同步隊列和多個等待隊列。
3. Condition 的實現
獲取一個Condition
必須要通過Lock
的newCondition()
方法。該方法定義在接口Lock
下面,返回的結果是綁定到此 Lock
實例的新 Condition
實例。
Condition
爲一個接口,其下僅有一個實現類ConditionObject
,由於Condition
的操作需要獲取相關的鎖,而AQS
則是同步鎖的實現基礎,所以ConditionObject
則定義爲AQS
的內部類。定義如下:
public class ConditionObject implements Condition, java.io.Serializable {
}
3.1 等待隊列
每個Condition
對象都包含着一個FIFO
隊列,該隊列是Condition
對象通知/等待功能的關鍵。在隊列中每一個節點都包含着一個線程引用,該線程就是在該Condition
對象上等待的線程。我們看Condition
的定義就明白了:
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
//頭節點
private transient Node firstWaiter;
//尾節點
private transient Node lastWaiter;
public ConditionObject() {
}
/** 省略方法 **/
}
從上面代碼可以看出Condition
擁有首節點(firstWaiter
),尾節點(lastWaiter
)。當前線程調用await()
方法,將會以當前線程構造成一個節點(Node
),並將節點加入到該隊列的尾部。結構如下:
Node
裏面包含了當前線程的引用。Node
定義與AQS
的CLH
同步隊列的節點使用的都是同一個類(AbstractQueuedSynchronized.Node
靜態內部類)。Condition
的隊列結構比CLH
同步隊列的結構簡單些,新增過程較爲簡單隻需要將原尾節點的nextWaiter
指向新增節點,然後更新lastWaiter
即可。
節點更新的過程並沒有使用CAS保證,原因在於調用await()方法的線程必定是獲取了鎖的線程,即該過程是由鎖來保證線程安全的。
3.2 等待
調用Condition
的await()
方法會使當前線程進入等待狀態,同時會加入到Condition
等待隊列同時釋放鎖。當從await()
方法返回時,當前線程一定是獲取了Condition
相關聯鎖。
public final void await() throws InterruptedException {
// 如果當前線程被中斷
if (Thread.interrupted())
throw new InterruptedException();
// 將當前線程加入等待隊列
Node node = addConditionWaiter();
// 釋放鎖
long 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) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
此段代碼的邏輯是:首先將當前線程新建一個節點同時加入到條件隊列中,然後釋放當前線程持有的同步狀態。然後則是不斷檢測該節點代表的線程釋放出現在CLH
同步隊列中(收到signal
信號之後就會在AQS
隊列中檢測到),如果不存在則一直掛起,否則參與競爭同步狀態。
加入條件隊列(addConditionWaiter()
)源碼如下:
private Node addConditionWaiter() {
Node t = lastWaiter; //尾節點
//Node的節點狀態如果不爲CONDITION,則表示該節點不處於等待狀態,需要清除節點
if (t != null && t.waitStatus != Node.CONDITION) {
//清除條件隊列中所有狀態不爲Condition的節點
unlinkCancelledWaiters();
t = lastWaiter;
}
//當前線程新建節點,狀態CONDITION
Node node = new Node(Thread.currentThread(), Node.CONDITION);
/**
* 將該節點加入到條件隊列中最後一個位置
*/
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
該方法主要是將當前線程加入到Condition
條件隊列中。當然在加入到尾節點之前會清除所有狀態不爲Condition
的節點。
fullyRelease(Node node)
,負責釋放該線程持有的鎖。
final long fullyRelease(Node node) {
boolean failed = true;
try {
//節點狀態--其實就是持有鎖的數量
long savedState = getState();
//釋放鎖
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
isOnSyncQueue(Node node)
:如果一個節點剛開始在條件隊列上,現在在同步隊列上獲取鎖則返回true
。
final boolean isOnSyncQueue(Node node) {
//狀態爲Condition,獲取前驅節點爲null,返回false
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
//後繼節點不爲null,判斷CLH隊列是否爲空,肯定在CLH同步隊列中
if (node.next != null)
return true;
return findNodeFromTail(node);
}
unlinkCancelledWaiters()
:負責將條件隊列中狀態不爲Condition
的節點刪除。
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
3.3 通知
調用Condition
的signal()
方法,將會喚醒在等待隊列中等待最長時間的節點(條件隊列裏的首節點),在喚醒節點前,會將節點移到CLH
同步隊列中。
public final void signal() {
//檢測當前線程是否爲擁有鎖的獨
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//頭節點,喚醒條件隊列中的第一個節點
Node first = firstWaiter;
if (first != null)
doSignal(first); //喚醒
}
該方法首先會判斷當前線程是否已經獲得了鎖,這是前置條件。然後喚醒條件隊列中的頭節點。
doSignal(Node first)
:喚醒頭節點。
private void doSignal(Node first) {
do {
//修改頭結點,完成舊頭結點的移出工作
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
doSignal(Node first)
主要是做兩件事:1.修改頭節點,2.調用transferForSignal(Node first)
方法將節點移動到CLH
同步隊列中。transferForSignal(Node first)
源碼如下:
final boolean transferForSignal(Node node) {
//將該節點從狀態CONDITION改變爲初始狀態0,
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//將節點加入到syn隊列中去,返回的是syn隊列中node節點前面的一個節點
Node p = enq(node);
int ws = p.waitStatus;
//如果結點p的狀態爲cancel 或者修改waitStatus失敗,則直接喚醒
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
整個通知的流程如下:
- 判斷當前線程是否已經獲取了鎖,如果沒有獲取則直接拋出異常,因爲獲取鎖爲通知的前置條件。
- 如果線程已經獲取了鎖,則將喚醒條件隊列的首節點
- 喚醒首節點是先將條件隊列中的頭節點移出,然後調用
AQS
的enq(Node node)
方法將其安全地移到CLH
同步隊列中 - 最後判斷如果該節點的同步狀態是否爲
Cancel
,或者修改狀態爲Signal
失敗時,則直接調用LockSupport
喚醒該節點的線程。
4. 總結
一個線程獲取鎖後,通過調用Condition
的await()
方法,會將當前線程先加入到條件隊列中,然後釋放鎖,最後通過isOnSyncQueue(Nodenode)
方法不斷自檢看節點是否已經在CLH
同步隊列了,如果是則嘗試獲取鎖,否則一直掛起。當線程調用signal()
方法後,程序首先檢查當前線程是否獲取了鎖,然後通過doSignal(Node first)
方法喚醒CLH
同步隊列的首節點。被喚醒的線程,將從await()
方法中的while
循環中退出來,然後調用acquireQueued()
方法競爭同步狀態。
5. Condition 的應用
下面是用Condition
實現的生產者消費者問題:
public class ConditionTest {
private LinkedList<String> buffer; //容器
private int maxSize ; //容器最大
private Lock lock;
private Condition fullCondition;
private Condition notFullCondition;
ConditionTest(int maxSize){
this.maxSize = maxSize;
buffer = new LinkedList<String>();
lock = new ReentrantLock();
fullCondition = lock.newCondition();
notFullCondition = lock.newCondition();
}
public void set(String string) throws InterruptedException {
lock.lock(); //獲取鎖
try {
while (maxSize == buffer.size()){
notFullCondition.await(); //滿了,添加的線程進入等待狀態
}
buffer.add(string);
fullCondition.signal();
} finally {
lock.unlock(); //記得釋放鎖
}
}
public String get() throws InterruptedException {
String string;
lock.lock();
try {
while (buffer.size() == 0){
fullCondition.await();
}
string = buffer.poll();
notFullCondition.signal();
} finally {
lock.unlock();
}
return string;
}
}