前言
前面我們學習線程的時候講過等待通知模式,之前講的是通過wait,notify/notifyAll配合synchronized關鍵字,實現等待通知,今天呢,我們介紹另外一種同樣實現等待通知模式的對象叫做condition接口,配合lock使用,也能完成等待通知,但是跟之前說的又有一些區別,今天就讓我們來認識一下吧,OK,開始我們今天的併發之旅吧,祝您旅途愉快。
什麼是Condition接口?
Condition定義了等待/通知兩種類型的方法,當前線程如果調用這些方法之前,必須先獲取到condition對象關聯的鎖,Condition對象是由Lock對象創建出來的,也就是說Condition是綁定在一個Lock對象上的,依賴於Lock對象,使用時需要通過Lock對象new出來。相比於之前的wait/notify而言,condition充當wait/notify的角色,而lock對象充當synchronized鎖角色。
我們看下Condition接口提供的一些方法如下:
如何使用Condition實現等待通知
案例1:一個condition即1個等待隊列
通過上面我們知道要使用condition提供的方法就必須將它綁定一個Lock對象,然後用法和wait/notify差不多,我們看下面demo
public class ConditionDemo {
// 創建重入鎖
private Lock lock = new ReentrantLock();
// Lock對象創建condition對象
private Condition condition = lock.newCondition();
public void method1(){
try {
lock.lock();
System.out.println("當前線程:" + Thread.currentThread().getName() + "獲取鎖,並睡眠3秒..");
Thread.sleep(3000);
System.out.println("當前線程:" + Thread.currentThread().getName() + "釋放鎖..進入等待狀態");
// 等待
condition.await();
System.out.println("當前線程:" + Thread.currentThread().getName() +"被喚醒,繼續執行...");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void method2(){
try {
lock.lock();
System.out.println("當前線程:" + Thread.currentThread().getName() + "獲取鎖,進入...睡眠3秒");
Thread.sleep(3000);
System.out.println("當前線程:" + Thread.currentThread().getName() + "發出喚醒..,並釋放鎖");
// 喚醒,同樣要注意多線程下死鎖的發生,優先使用signalAll()喚醒所有等待此條件的線程
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws Exception{
final ConditionDemo uc = new ConditionDemo();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
uc.method1();
}
}, "線程1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
uc.method2();
}
}, "線程2");
System.out.println("線程1啓動。。。");
t1.start();
Thread.sleep(1000);
System.out.println("線程2啓動。。。");
t2.start();
}
}
執行結果如下:
案例2:多個Condition,即多個等待隊列
public class ConditionDemo {
// 定義重入鎖
private ReentrantLock lock = new ReentrantLock();
// condition1
private Condition c1 = lock.newCondition();
// condition2
private Condition c2 = lock.newCondition();
public void m1(){
try {
lock.lock();
System.out.println("當前線程:" +Thread.currentThread().getName() + "進入方法m1等待..");
c1.await();
System.out.println("當前線程:" +Thread.currentThread().getName() + "方法m1繼續..");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void m2(){
try {
lock.lock();
System.out.println("當前線程:" +Thread.currentThread().getName() + "進入方法m2等待..");
c1.await();
System.out.println("當前線程:" +Thread.currentThread().getName() + "方法m2繼續..");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void m3(){
try {
lock.lock();
System.out.println("當前線程:" +Thread.currentThread().getName() + "進入方法m3等待..");
c2.await();
System.out.println("當前線程:" +Thread.currentThread().getName() + "方法m3繼續..");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void m4(){
try {
lock.lock();
System.out.println("當前線程:" +Thread.currentThread().getName() + "喚醒..");
// 喚醒所有condition1上的等待線程
c1.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void m5(){
try {
lock.lock();
System.out.println("當前線程:" +Thread.currentThread().getName() + "喚醒..");
// 喚醒所有condition2上的等待線程
c2.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
final ConditionDemo umc = new ConditionDemo();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
umc.m1();
}
},"Thread1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
umc.m2();
}
},"Thread2");
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
umc.m3();
}
},"Thread3");
Thread t4 = new Thread(new Runnable() {
@Override
public void run() {
umc.m4();
}
},"Thread4");
Thread t5 = new Thread(new Runnable() {
@Override
public void run() {
umc.m5();
}
},"Thread5");
// condition1
t1.start();
// condition1
t2.start();
// condition2
t3.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 喚醒condition1
t4.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 喚醒condition2
t5.start();
}
}
執行結果如下:
Condition底層實現
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();
}
具體實現主要主要體現在ConditionObject這個類上,這個類是AQS的內部類,每個ConditionObject對象都包含一個等待隊列,下面我們就分別從等待隊列,等待,通知三個方面分析ConditionObject的底層實現原理
等待隊列
一個condition包含一個等待隊列(FIFO),condition擁有一個首節點和一個尾節點,如下圖:condition中擁有首位節點的引用,當新增節點時,只需要將原來尾節點的下個節點指向它,並且更新尾節點接口,該更新操作跟之前同步隊列中CAS更新尾節點不同,此處不需要CAS操作,因爲condition的操作是在一個lock裏面進行的,是已經獲取鎖的,所以這個操作是線程安全的;
在前面所講的wait等待通知模型中其實同步器是隻有一個同步隊列和一個等待隊列的,而在我們這裏,因爲可以同時多個condition,上面案例也有使用過,也就是說,condition實現的同步器中,其實是一個同步隊列和多個等待隊列,從condition是AQS一個內部類也可以證實這一點,也就是說我們可以創建多個condition,每個condition都可以訪問AQS提供的方法,相當於每個condition都持有所屬AQS的引用,其關係模型如下:
等待
調用Condition的await開頭的系列方法,會使當前線程進入等待隊列等待並釋放鎖,線程狀態變爲等待狀態,已上圖爲例,就是同步隊列中的首節點(或者獲取鎖的節點,因爲非公平性鎖就不一定是首節點)移動到了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;
}
// 調用acquireQueued嘗試獲取同步狀態,獲取成功後,線程中斷返回
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
整個過程示意圖如下:
通知
調用Condition的signal()方法將會喚醒在等待隊列中的首節點,該節點也是到目前爲止等待時間最長的節點,等待隊列遵循FIFO原則。調用signalAll()方法將會喚醒該同步器上等待隊列中的所有節點,我們看signal方法源碼分析:
public final void signal() {
// 前置檢查,判斷當前線程是否是獲取了鎖的線程,如果不是拋出異常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 取得等待隊列的頭結點,頭結點不爲空執行doSignal,否則,喚醒結束
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
我們再來看下doSignal的源碼:
private void doSignal(Node first) {
// 調用transferForSignal將節點從等待隊列移動到同步隊列
// 將該節點從等待隊列刪除
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
再往下看,我們追溯到transferForSignal方法:
final boolean transferForSignal(Node node) {
// 將節點waitStatus設置爲0
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 調用enq方法將該節點加入同步隊列
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// 使用LockSuppor.unpark()方法喚醒該節點的線程
LockSupport.unpark(node.thread);
return true;
}
signalAll()方法實現也差不多,我就不做過多講解了,看下doSignlAll方法即可:
/**
* Removes and transfers all nodes.
* @param first (non-null) the first node on condition queue
*/
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
我們再來看下節點在隊列中的變化過程,如下圖所示:
以上就是今天等待通知機制condition的全部內容,結合wait/notify+synchronized,對比學習,希望能對您有所收穫!!!