因為wait()、notify()是和synchronized配合使用的,因此如果使用了顯示鎖Lock,就不能用了。所以顯示鎖要提供自己的等待/通知機制,Condition應運而生。
Condition中的
await()
方法相當於Object的wait()
方法,Condition中的signal()
方法相當於Object的notify()
方法,Condition中的signalAll()
相當於Object的notifyAll()
方法。不同的是,Object中的wait(),notify(),notifyAll()
方法是和"同步鎖"
(synchronized關鍵字)捆綁使用的;而Condition是需要與"互斥鎖"/"共享鎖"
捆綁使用的。
1. 函式列表
-
void await() throws InterruptedException
當前執行緒進入等待狀態,直到被通知(signal)或者被中斷時,當前執行緒進入執行狀態,從await()返回; -
void awaitUninterruptibly()
當前執行緒進入等待狀態,直到被通知,對中斷不做響應; -
long awaitNanos(long nanosTimeout) throws InterruptedException
在介面1的返回條件基礎上增加了超時響應,返回值表示當前剩餘的時間,如果在nanosTimeout之前被喚醒,返回值 = nanosTimeout - 實際消耗的時間,返回值 <= 0表示超時; -
boolean await(long time, TimeUnit unit) throws InterruptedException
同樣是在介面1的返回條件基礎上增加了超時響應,與介面3不同的是: 可以自定義超時時間單位; 返回值返回true/false,在time之前被喚醒,返回true,超時返回false。 -
boolean awaitUntil(Date deadline) throws InterruptedException
當前執行緒進入等待狀態直到將來的指定時間被通知,如果沒有到指定時間被通知返回true,否則,到達指定時間,返回false; -
void signal()
喚醒一個等待在Condition上的執行緒; -
void signalAll()
喚醒等待在Condition上所有的執行緒。
2. 具體實現
看到電腦上當初有幫人做過一道題,就拿它做例項演示:
編寫一個Java應用程式,要求有三個程式:student1,student2,teacher,其中執行緒student1準備“睡”1分鐘後再開始上課,執行緒student2準備“睡”5分鐘後再開始上課。Teacher在輸出4句“上課”後,“喚醒”了休眠的執行緒student1;執行緒student1被“喚醒”後,負責再“喚醒”休眠的執行緒student2.
2.1 實現一:
基於Object和synchronized實現。
package com.fantJ.bigdata;
/**
* Created by Fant.J.
* 2018/7/2 16:36
*/
public class Ten {
static class Student1{
private boolean student1Flag = false;
public synchronized boolean isStudent1Flag() {
System.out.println("學生1開始睡覺1min");
if (!this.student1Flag){
try {
System.out.println("學生1睡著了");
wait(1*1000*60);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("學生1被喚醒");
return student1Flag;
}
public synchronized void setStudent1Flag(boolean student1Flag) {
this.student1Flag = student1Flag;
notify();
}
}
static class Student2{
private boolean student2Flag = false;
public synchronized boolean isStudent2Flag() {
System.out.println("學生2開始睡覺5min");
if (!this.student2Flag){
try {
System.out.println("學生2睡著了");
wait(5*1000*60);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("學生2被喚醒");
return student2Flag;
}
public synchronized void setStudent2Flag(boolean student2Flag) {
notify();
this.student2Flag = student2Flag;
}
}
static class Teacher{
private boolean teacherFlag = true;
public synchronized boolean isTeacherFlag() {
if (!this.teacherFlag){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("老師準備吼著要上課");
return teacherFlag;
}
public synchronized void setTeacherFlag(boolean teacherFlag) {
this.teacherFlag = teacherFlag;
notify();
}
}
public static void main(String[] args) {
Student1 student1 = new Student1();
Student2 student2 = new Student2();
Teacher teacher = new Teacher();
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0;i<4;i++){
System.out.println("上課");
}
teacher.isTeacherFlag();
System.out.println("學生1被吵醒了,1s後反應過來");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
student1.setStudent1Flag(true);
}
});
Thread s1 = new Thread(new Runnable() {
@Override
public void run() {
student1.isStudent1Flag();
System.out.println("準備喚醒學生2,喚醒需要1s");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
student2.setStudent2Flag(true);
}
});
Thread s2 = new Thread(new Runnable() {
@Override
public void run() {
student2.isStudent2Flag();
}
});
s1.start();
s2.start();
t.start();
}
}
複製程式碼
當然,用notifyAll
可能會用更少的程式碼,這種實現方式雖然複雜,單效能上會比使用notifyAll()
要強很多,因為沒有鎖爭奪導致的資源浪費。但是可以看到,程式碼很複雜,例項與例項之間也需要保證很好的隔離。
2.2 實現二:
基於Condition、ReentrantLock實現。
public class xxx{
private int signal = 0;
public Lock lock = new ReentrantLock();
Condition teacher = lock.newCondition();
Condition student1 = lock.newCondition();
Condition student2 = lock.newCondition();
public void teacher(){
lock.lock();
while (signal != 0){
try {
teacher.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("老師叫上課");
signal++;
student1.signal();
lock.unlock();
}
public void student1(){
lock.lock();
while (signal != 1){
try {
student1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("學生1醒了,準備叫醒學生2");
signal++;
student2.signal();
lock.unlock();
}
public void student2(){
lock.lock();
while (signal != 2){
try {
student2.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("學生2醒了");
signal=0;
teacher.signal();
lock.unlock();
}
public static void main(String[] args) {
ThreadCommunicate2 ten = new ThreadCommunicate2();
new Thread(() -> ten.teacher()).start();
new Thread(() -> ten.student1()).start();
new Thread(() -> ten.student2()).start();
}
}
複製程式碼
Condition
依賴於Lock
介面,生成一個Condition的基本程式碼是lock.newCondition()
呼叫Condition
的await()
和signal()
方法,都必須在lock
保護之內,就是說必須在lock.lock()
和lock.unlock
之間才可以使用。
可以觀察到,我取消了synchronized
方法關鍵字,在每個加鎖的方法前後分別加了lock.lock(); lock.unlock();
來獲取/施放鎖,並且在釋放鎖之前施放想要施放的Condition
物件。同樣的,我們使用signal
來完成執行緒間的通訊。
3. Condition實現有界佇列
為什麼要用它來實現有界佇列呢,因為我們可以利用Condition來實現阻塞(當佇列空或者滿的時候)。這就為我們減少了很多的麻煩。
public class MyQueue<E> {
private Object[] objects;
private Lock lock = new ReentrantLock();
private Condition addCDT = lock.newCondition();
private Condition rmCDT = lock.newCondition();
private int addIndex;
private int rmIndex;
private int queueSize;
MyQueue(int size){
objects = new Object[size];
}
public void add(E e){
lock.lock();
while (queueSize == objects.length){
try {
addCDT.await();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
objects[addIndex] = e;
System.out.println("新增了資料"+"Objects["+addIndex+"] = "+e);
if (++addIndex == objects.length){
addIndex = 0;
}
queueSize++;
rmCDT.signal();
lock.unlock();
}
public Object remove(){
lock.lock();
while (queueSize == 0){
try {
System.out.println("佇列為空");
rmCDT.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Object temp = objects[rmIndex];
objects[rmIndex] = null;
System.out.println("移除了資料"+"Objects["+rmIndex+"] = null");
if (++rmIndex == objects.length){
rmIndex = 0;
}
queueSize--;
addCDT.signal();
lock.unlock();
return temp;
}
public void foreach(E e){
if (e instanceof String){
Arrays.stream(objects).map(obj->{
if (obj == null){
obj = " ";
}
return obj;
}).map(Object::toString).forEach(System.out::println);
}
if (e instanceof Integer){
Arrays.stream(objects).map(obj -> {
if (obj == null ){
obj = 0;
}
return obj;
}).map(object -> Integer.valueOf(object.toString())).forEach(System.out::println);
}
}
}
複製程式碼
add
方法就是往佇列中新增資料。
remove
是從佇列中按FIFO移除資料。
foreach
方法是一個觀察佇列內容的工具方法,很容易看出,它是用來遍歷的。
public static void main(String[] args) {
MyQueue<Integer> myQueue = new MyQueue<>(5);
myQueue.add(5);
myQueue.add(4);
myQueue.add(3);
// myQueue.add(2);
// myQueue.add(1);
myQueue.remove();
myQueue.foreach(5);
}
複製程式碼
新增了資料Objects[0] = 5
新增了資料Objects[1] = 4
新增了資料Objects[2] = 3
移除了資料Objects[0] = null
0
4
3
0
0
複製程式碼
4. 原始碼分析
ReentrantLock.class
public Condition newCondition() {
return sync.newCondition();
}
複製程式碼
sync溯源:
private final Sync sync;
複製程式碼
Sync類中有一個newCondition()方法:
final ConditionObject newCondition() {
return new ConditionObject();
}
複製程式碼
public class ConditionObject implements Condition, java.io.Serializable {
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
}
複製程式碼
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原始碼:
public final void await() throws InterruptedException {
// 1.如果當前執行緒被中斷,則丟擲中斷異常
if (Thread.interrupted())
throw new InterruptedException();
// 2.將節點加入到Condition佇列中去,這裡如果lastWaiter是cancel狀態,那麼會把它踢出Condition佇列。
Node node = addConditionWaiter();
// 3.呼叫tryRelease,釋放當前執行緒的鎖
long savedState = fullyRelease(node);
int interruptMode = 0;
// 4.為什麼會有在AQS的等待佇列的判斷?
// 解答:signal操作會將Node從Condition佇列中拿出並且放入到等待佇列中去,在不在AQS等待佇列就看signal是否執行了
// 如果不在AQS等待佇列中,就park當前執行緒,如果在,就退出迴圈,這個時候如果被中斷,那麼就退出迴圈
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 5.這個時候執行緒已經被signal()或者signalAll()操作給喚醒了,退出了4中的while迴圈
// 自旋等待嘗試再次獲取鎖,呼叫acquireQueued方法
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
複製程式碼
-
將當前執行緒加入
Condition
鎖佇列。特別說明的是,這裡不同於AQS
的佇列,這裡進入的是Condition
的FIFO
佇列。 -
釋放鎖。這裡可以看到將鎖釋放了,否則別的執行緒就無法拿到鎖而發生死鎖。
-
自旋
(while
)掛起,直到被喚醒(signal
把他重新放回到AQS的等待佇列)或者超時或者CACELLED等。 -
獲取鎖
(acquireQueued
)。並將自己從Condition
的FIFO
佇列中釋放,表明自己不再需要鎖(我已經拿到鎖了)。
signal()原始碼
public final void signal() {
if (!isHeldExclusively())
//如果同步狀態不是被當前執行緒獨佔,直接丟擲異常。從這裡也能看出來,Condition只能配合獨佔類同步元件使用。
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
//通知等待佇列隊首的節點。
doSignal(first);
}
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) && //transferForSignal方法嘗試喚醒當前節點,如果喚醒失敗,則繼續嘗試喚醒當前節點的後繼節點。
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
//如果當前節點狀態為CONDITION,則將狀態改為0準備加入同步佇列;如果當前狀態不為CONDITION,說明該節點等待已被中斷,則該方法返回false,doSignal()方法會繼續嘗試喚醒當前節點的後繼節點
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
Node p = enq(node); //將節點加入同步佇列,返回的p是節點在同步佇列中的先驅節點
int ws = p.waitStatus;
//如果先驅節點的狀態為CANCELLED(>0) 或設定先驅節點的狀態為SIGNAL失敗,那麼就立即喚醒當前節點對應的執行緒,執行緒被喚醒後會執行acquireQueued方法,該方法會重新嘗試將節點的先驅狀態設為SIGNAL並再次park執行緒;如果當前設定前驅節點狀態為SIGNAL成功,那麼就不需要馬上喚醒執行緒了,當它的前驅節點成為同步佇列的首節點且釋放同步狀態後,會自動喚醒它。
//其實筆者認為這裡不加這個判斷條件應該也是可以的。只是對於CAS修改前驅節點狀態為SIGNAL成功這種情況來說,如果不加這個判斷條件,提前喚醒了執行緒,等進入acquireQueued方法了節點發現自己的前驅不是首節點,還要再阻塞,等到其前驅節點成為首節點並釋放鎖時再喚醒一次;而如果加了這個條件,執行緒被喚醒的時候它的前驅節點肯定是首節點了,執行緒就有機會直接獲取同步狀態從而避免二次阻塞,節省了硬體資源。
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
複製程式碼
signal
就是喚醒Condition
佇列中的第一個非CANCELLED節點執行緒,而signalAll就是喚醒所有非CANCELLED
節點執行緒,本質是將節點從Condition
佇列中取出來一個還是所有節點放到AQS
的等待佇列。儘管所有Node
可能都被喚醒,但是要知道的是仍然只有一個執行緒能夠拿到鎖,其它沒有拿到鎖的執行緒仍然需要自旋等待,就上上面提到的第4步(acquireQueued
)。
實現過程概述
我們知道
Lock的本質是AQS
,AQS
自己維護的佇列是當前等待資源的佇列,AQS
會在資源被釋放後,依次喚醒佇列中從前到後的所有節點,使他們對應的執行緒恢復執行,直到佇列為空。而Condition
自己也維護了一個佇列,該佇列的作用是維護一個等待signal
訊號的佇列。但是,兩個佇列的作用不同的,事實上,每個執行緒也僅僅會同時存在以上兩個佇列中的一個,流程是這樣的:
- 執行緒1呼叫
reentrantLock.lock
時,嘗試獲取鎖。如果成功,則返回,從AQS
的佇列中移除執行緒;否則阻塞,保持在AQS
的等待佇列中。 - 執行緒1呼叫
await
方法被呼叫時,對應操作是被加入到Condition
的等待佇列中,等待signal訊號;同時釋放鎖。 - 鎖被釋放後,會喚醒AQS佇列中的頭結點,所以執行緒2會獲取到鎖。
- 執行緒2呼叫
signal
方法,這個時候Condition
的等待佇列中只有執行緒1一個節點,於是它被取出來,並被加入到AQS的等待佇列中。注意,這個時候,執行緒1 並沒有被喚醒,只是被加入AQS等待佇列。 signal
方法執行完畢,執行緒2呼叫unLock()
方法,釋放鎖。這個時候因為AQS中只有執行緒1,於是,執行緒1被喚醒,執行緒1恢復執行。 所以: 傳送signal
訊號只是將Condition
佇列中的執行緒加到AQS
的等待佇列中。只有到傳送signal
訊號的執行緒呼叫reentrantLock.unlock()
釋放鎖後,這些執行緒才會被喚醒。
可以看到,整個協作過程是靠結點在AQS
的等待佇列和Condition
的等待佇列中來回移動實現的,Condition
作為一個條件類,很好的自己維護了一個等待訊號的佇列,並在適時的時候將結點加入到AQS
的等待佇列中來實現的喚醒操作。
Condition等待通知的本質請參考:www.cnblogs.com/sheeva/p/64…