(轉載)深入剖析基于并發AQS的(獨占鎖)重入鎖(ReetrantLock)及其Condition實現原理(二)

原文鏈接:深入剖析基于并發AQS的(獨占鎖)重入鎖(ReetrantLock)及其Condition實現原理 - CSDN博客

神奇的Condition

關于Condition接口

在并發編程中,每個Java對象都存在一組監視器方法,如wait()、notify()以及notifyAll()方法,通過這些方法,我們可以實現線程間通信與協作(也稱為等待喚醒機制),如生產者-消費者模式,而且這些方法必須配合著synchronized關鍵字使用,關于這點,如果想有更深入的理解,可觀看博主另外一篇博文【?深入理解Java并發之synchronized實現原理】,與synchronized的等待喚醒機制相比Condition具有更多的靈活性以及精確性,這是因為notify()在喚醒線程時是隨機(同一個鎖),而Condition則可通過多個Condition實例對象建立更加精細的線程控制,也就帶來了更多靈活性了,我們可以簡單理解為以下兩點:

1. 通過Condition能夠精細的控制多線程的休眠與喚醒。

2. 對于一個鎖,我們可以為多個線程間建立不同的Condition。

Condition是一個接口類,其主要方法如下:

public interfaceCondition{

?/**

? * 使當前線程進入等待狀態直到被通知(signal)或中斷

? * 當其他線程調用singal()或singalAll()方法時,該線程將被喚醒

? * 當其他線程調用interrupt()方法中斷當前線程

? * await()相當于synchronized等待喚醒機制中的wait()方法

? */

?void await() throws InterruptedException;

//當前線程進入等待狀態,直到被喚醒,該方法不響應中斷要求

?void awaitUninterruptibly();

//調用該方法,當前線程進入等待狀態,直到被喚醒或被中斷或超時

?//其中nanosTimeout指的等待超時時間,單位納秒

?long awaitNanos(long nanosTimeout) throws InterruptedException;

? //同awaitNanos,但可以指明時間單位

? boolean await(long time, TimeUnit unit) throws InterruptedException;

//調用該方法當前線程進入等待狀態,直到被喚醒、中斷或到達某個時

?//間期限(deadline),如果沒到指定時間就被喚醒,返回true,其他情況返回false

? boolean awaitUntil(Date deadline) throws InterruptedException;

//喚醒一個等待在Condition上的線程,該線程從等待方法返回前必須

?//獲取與Condition相關聯的鎖,功能與notify()相同

? void signal();

//喚醒所有等待在Condition上的線程,該線程從等待方法返回前必須

?//獲取與Condition相關聯的鎖,功能與notifyAll()相同

? void signalAll();

}

關于Condition的實現類是AQS的內部類ConditionObject,關于這點我們稍后分析,這里先來看一個Condition的使用案例,即經典消費者生產者模式。

Condition的使用案例-生產者消費者模式

這里我們通過一個賣烤鴨的案例來演示多生產多消費者的案例,該場景中存在兩條生產線程t1和t2,用于生產烤鴨,也存在兩條消費線程t3,t4用于消費烤鴨,4條線程同時執行,需要保證只有在生產線程產生烤鴨后,消費線程才能消費,否則只能等待,直到生產線程產生烤鴨后喚醒消費線程,注意烤鴨不能重復消費。ResourceByCondition類中定義product()和consume()兩個方法,分別用于生產烤鴨和消費烤鴨,并且定義ReentrantLock鎖,用于控制product()和consume()的并發,由于必須在烤鴨生成完成后消費線程才能消費烤鴨,否則只能等待,因此這里定義兩組Condition對象,分別是producer_con和consumer_con,前者擁有控制生產線程,后者擁有控制消費線程,這里我們使用一個標志flag來控制是否有烤鴨,當flag為true時,代表烤鴨生成完畢,生產線程必須進入等待狀態同時喚醒消費線程進行消費,消費線程消費完畢后將flag設置為false,代表烤鴨消費完成,進入等待狀態,同時喚醒生產線程生產烤鴨,具體代碼如下:

package com.zejian.concurrencys;

import java.util.concurrent.locks.Condition;

import java.util.concurrent.locks.Lock;

import java.util.concurrent.locks.ReentrantLock;

/**

* Created by zejian on 2017/7/22.

* Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創]

*/

public class ResourceByCondition{

? ? private String name;

? ? private int count = 1;

? ? private boolean flag = false;

? ? //創建一個鎖對象。

? ? Lock lock = new ReentrantLock();

? ? //通過已有的鎖獲取兩組監視器,一組監視生產者,一組監視消費者。??

? ? Condition producer_con = lock.newCondition();

? ? Condition consumer_con = lock.newCondition();

? ? /**

? ? * 生產

? ? * @paramname

? ? */

? ? public? void product(String name)

? ? {

? ? ? ? lock.lock();

? ? ? ? try? ? ? ? {

? ? ? ? ? ? while(flag){

? ? ? ? ? ? ? ? try{

????????????????????producer_con.await();

????????????????}catch(InterruptedException e){}

? ? ? ? ? ? }

? ? ? ? ? ? this.name = name + count;

? ? ? ? ? ? count++;

? ? ? ? ? ? System.out.println(Thread.currentThread().getName()+"...生產者5.0..."+this.name);

? ? ? ? ? ? flag = true;

? ? ? ? ? ? consumer_con.signal();//直接喚醒消費線程

? ? ? ? }

? ? ? ? finally? ? ? ? {

? ? ? ? ? ? lock.unlock();

? ? ? ? }

? ? }

? ? /**

? ? * 消費

? ? */

? ? public? void consume()

? ? {

? ? ? ? lock.lock();

? ? ? ? try? ? ? ? {

? ? ? ? ? ? while(!flag){

? ? ? ? ? ? ? ? try{

????????????????????consumer_con.await();

????????????????}catch(InterruptedException e){}

? ? ? ? ? ? }

? ? ? ? ? ? System.out.println(Thread.currentThread().getName()+"...消費者.5.0......."+this.name);//消費烤鴨1? ? ? ? ? ??

????????????flag = false;

? ? ? ? ? ? producer_con.signal();//直接喚醒生產線程

? ? ? ? }

? ? ? ? finally

? ? ? ? {

? ? ? ? ? ? lock.unlock();

? ? ? ? }

? ? }

}

執行代碼

package com.zejian.concurrencys;

/**

* Created by zejian on 2017/7/22.

* Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創]

*/

public classMutil_Producer_ConsumerByCondition{

? ? public static void main(String[] args) {

? ? ? ? ResourceByCondition r = new ResourceByCondition();

? ? ? ? Mutil_Producer pro = new Mutil_Producer(r);

? ? ? ? Mutil_Consumer con = new Mutil_Consumer(r);

? ? ? ? //生產者線程

? ? ? ? Thread t0 = new Thread(pro);

? ? ? ? Thread t1 = new Thread(pro);

? ? ? ? //消費者線程

? ? ? ? Thread t2 = new Thread(con);

? ? ? ? Thread t3 = new Thread(con);

? ? ? ? //啟動線程

? ? ? ? t0.start();

? ? ? ? t1.start();

? ? ? ? t2.start();

? ? ? ? t3.start();

? ? }

}

/** * @decrition生產者線程 */

class Mutil_Producer implements Runnable {

? ? private ResourceByCondition r;

? ? Mutil_Producer(ResourceByCondition r) {

? ? ? ? this.r = r;

? ? }

? ? public void run() {

? ? ? ? while (true) {

? ? ? ? ? ? r.product("北京烤鴨");

? ? ? ? }

? ? }

}

/** * @decrition消費者線程 */

class Mutil_Consumer implements Runnable {

? ? private ResourceByCondition r;

? ? Mutil_Consumer(ResourceByCondition r) {

? ? ? ? this.r = r;

? ? }

? ? public void run() {

? ? ? ? while (true) {

? ? ? ? ? ? r.consume();

? ? ? ? }

? ? }

}

正如代碼所示,我們通過兩者Condition對象單獨控制消費線程與生產消費,這樣可以避免消費線程在喚醒線程時喚醒的還是消費線程,如果是通過synchronized的等待喚醒機制實現的話,就可能無法避免這種情況,畢竟同一個鎖,對于synchronized關鍵字來說只能有一組等待喚醒隊列,而不能像Condition一樣,同一個鎖擁有多個等待隊列。synchronized的實現方案如下,

public classKaoYaResource{

?private String name;

? ? private int count = 1;//烤鴨的初始數量?

?? private boolean flag = false;//判斷是否有需要線程等待的標志

? ? /**

? ? * 生產烤鴨

? ? */

? ? public synchronized void product(String name){

? ? ? ? while(flag){

? ? ? ? ? ? //此時有烤鴨,等待

? ? ? ? ? ? try {

? ? ? ? ? ? ? ? this.wait();

? ? ? ? ? ? } catch (InterruptedException e) {

? ? ? ? ? ? ? ? e.printStackTrace();

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? this.name=name+count;//設置烤鴨的名稱

? ? ? ? count++;

? ? ? ? System.out.println(Thread.currentThread().getName()+"...生產者..."+this.name);

? ? ? ? flag=true;//有烤鴨后改變標志

? ? ? ? notifyAll();//通知消費線程可以消費了

? ? }

? ? /**

? ? * 消費烤鴨

? ? */

? ? public synchronized void consume(){

? ? ? ? while(!flag){//如果沒有烤鴨就等待

? ? ? ? ? ? try{this.wait();}catch(InterruptedException e){}

? ? ? ? }

? ? ? ? System.out.println(Thread.currentThread().getName()+"...消費者........"+this.name);//消費烤鴨1? ? ? ? flag = false;

? ? ? ? notifyAll();//通知生產者生產烤鴨

? ? }

}

如上代碼,在調用notify()或者 notifyAll()方法時,由于等待隊列中同時存在生產者線程和消費者線程,所以我們并不能保證被喚醒的到底是消費者線程還是生產者線程,而Codition則可以避免這種情況。嗯,了解完Condition的使用方式后,下面我們將進一步探討Condition背后的實現機制。

Condition的實現原理

Condition的具體實現類是AQS的內部類ConditionObject,前面我們分析過AQS中存在兩種隊列,一種是同步隊列,一種是等待隊列,而等待隊列就相對于Condition而言的。注意在使用Condition前必須獲得鎖,同時在Condition的等待隊列上的結點與前面同步隊列的結點是同一個類即Node,其結點的waitStatus的值為CONDITION。在實現類ConditionObject中有兩個結點分別是firstWaiter和lastWaiter,firstWaiter代表等待隊列第一個等待結點,lastWaiter代表等待隊列最后一個等待結點,如下:

public class ConditionObject implements Condition,java.io.Serializable{

?//等待隊列第一個等待結點

?private transient Node firstWaiter;

? ? //等待隊列最后一個等待結點

? ? private transient Node lastWaiter;

? ? //省略其他代碼.......

}

每個Condition都對應著一個等待隊列,也就是說如果一個鎖上創建了多個Condition對象,那么也就存在多個等待隊列。等待隊列是一個FIFO的隊列,在隊列中每一個節點都包含了一個線程的引用,而該線程就是Condition對象上等待的線程。當一個線程調用了await()相關的方法,那么該線程將會釋放鎖,并構建一個Node節點封裝當前線程的相關信息加入到等待隊列中進行等待,直到被喚醒、中斷、超時才從隊列中移出。Condition中的等待隊列模型如下:

正如圖所示,Node節點的數據結構,在等待隊列中使用的變量與同步隊列是不同的,Condtion中等待隊列的結點只有直接指向的后繼結點并沒有指明前驅結點,而且使用的變量是nextWaiter而不是next,這點我們在前面分析結點Node的數據結構時講過。firstWaiter指向等待隊列的頭結點,lastWaiter指向等待隊列的尾結點,等待隊列中結點的狀態只有兩種即CANCELLED和CONDITION,前者表示線程已結束需要從等待隊列中移除,后者表示條件結點等待被喚醒。再次強調每個Codition對象對于一個等待隊列,也就是說AQS中只能存在一個同步隊列,但可擁有多個等待隊列。下面從代碼層面看看被調用await()方法(其他await()實現原理類似)的線程是如何加入等待隊列的,而又是如何從等待隊列中被喚醒的:

public final void await() throws InterruptedException {

? ? ? //判斷線程是否被中斷

? ? ? if (Thread.interrupted())

? ? ? ? ? throw new InterruptedException();

? ? ? //創建新結點加入等待隊列并返回

? ? ? Node node = addConditionWaiter();

? ? ? //釋放當前線程鎖即釋放同步狀態

? ? ? int savedState = fullyRelease(node);

? ? ? int interruptMode = 0;

? ? ? //判斷結點是否同步隊列(SyncQueue)中,即是否被喚醒

? ? ? while (!isOnSyncQueue(node)) {

? ? ? ? ? //掛起線程

? ? ? ? ? LockSupport.park(this);

? ? ? ? ? //判斷是否被中斷喚醒,如果是退出循環。

? ? ? ? ? if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)

? ? ? ? ? ? ? break;

? ? ? }

? ? ? //被喚醒后執行自旋操作爭取獲得鎖,同時判斷線程是否被中斷

? ? ? if (acquireQueued(node, savedState) && interruptMode != THROW_IE)

? ? ? ? ? interruptMode = REINTERRUPT;

? ? ? // clean up if cancelled

? ? ? if (node.nextWaiter != null)

? ? ? ? ? //清理等待隊列中不為CONDITION狀態的結點

? ? ? ? ? unlinkCancelledWaiters();

? ? ? if (interruptMode != 0)

? ? ? ? ? reportInterruptAfterWait(interruptMode);

? }

執行addConditionWaiter()添加到等待隊列。

private Node addConditionWaiter() {

? ? Node t = lastWaiter;

? ? ? // 判斷是否為結束狀態的結點并移除

? ? ? if (t != null && t.waitStatus != Node.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;

}

await()方法主要做了3件事,一是調用addConditionWaiter()方法將當前線程封裝成node結點加入等待隊列,二是調用fullyRelease(node)方法釋放同步狀態并喚醒后繼結點的線程。三是調用isOnSyncQueue(node)方法判斷結點是否在同步隊列中,注意是個while循環,如果同步隊列中沒有該結點就直接掛起該線程,需要明白的是如果線程被喚醒后就調用acquireQueued(node, savedState)執行自旋操作爭取鎖,即當前線程結點從等待隊列轉移到同步隊列并開始努力獲取鎖。

接著看看喚醒操作singal()方法:

public final void signal() {

? ? //判斷是否持有獨占鎖,如果不是拋出異常

? if (!isHeldExclusively())

? ? ? ? ? throw new IllegalMonitorStateException();

? ? ? Node first = firstWaiter;

? ? ? //喚醒等待隊列第一個結點的線程

? ? ? if (first != null)

? ? ? ? ? doSignal(first);

}

這里signal()方法做了兩件事,一是判斷當前線程是否持有獨占鎖,沒有就拋出異常,從這點也可以看出只有獨占模式先采用等待隊列,而共享模式下是沒有等待隊列的,也就沒法使用Condition。二是喚醒等待隊列的第一個結點,即執行doSignal(first):

private void doSignal(Node first) {

? ? do {

? ? ? ? ? ? //移除條件等待隊列中的第一個結點,

? ? ? ? ? ? //如果后繼結點為null,那么說沒有其他結點將尾結點也設置為null

? ? ? ? ? ? if ( (firstWaiter = first.nextWaiter) == null)

? ? ? ? ? ? ? ? lastWaiter = null;

? ? ? ? ? ? first.nextWaiter = null;

? ? ? ? ? //如果被通知節點沒有進入到同步隊列并且條件等待隊列還有不為空的節點,則繼續循環通知后續結點

? ? ? ? } while (!transferForSignal(first) &&

? ? ? ? ? ? ? ? ? (first = firstWaiter) != null);

? ? ? ? }

//transferForSignal方法

final boolean transferForSignal(Node node) {

? ? //嘗試設置喚醒結點的waitStatus為0,即初始化狀態

? ? //如果設置失敗,說明當期結點node的waitStatus已不為

? ? //CONDITION狀態,那么只能是結束狀態了,因此返回false

? ? //返回doSignal()方法中繼續喚醒其他結點的線程,注意這里并

? ? //不涉及并發問題,所以CAS操作失敗只可能是預期值不為CONDITION,

? ? //而不是多線程設置導致預期值變化,畢竟操作該方法的線程是持有鎖的。

? ? if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))

? ? ? ? return false;

? ? ? ? //加入同步隊列并返回前驅結點p?

?? ? ? Node p = enq(node);

? ? ? ? int ws = p.waitStatus;

? ? ? ? //判斷前驅結點是否為結束結點(CANCELLED=1)或者在設置

? ? ? ? //前驅節點狀態為Node.SIGNAL狀態失敗時,喚醒被通知節點代表的線程

? ? ? ? if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))

? ? ? ? ? ? //喚醒node結點的線程

? ? ? ? ? ? LockSupport.unpark(node.thread);

? ? ? ? return true;

? ? }

注釋說得很明白了,這里我們簡單整體說明一下,doSignal(first)方法中做了兩件事,從條件等待隊列移除被喚醒的節點,然后重新維護條件等待隊列的firstWaiter和lastWaiter的指向。二是將從等待隊列移除的結點加入同步隊列(在transferForSignal()方法中完成的),如果進入到同步隊列失敗并且條件等待隊列還有不為空的節點,則繼續循環喚醒后續其他結點的線程。到此整個signal()的喚醒過程就很清晰了,即signal()被調用后,先判斷當前線程是否持有獨占鎖,如果有,那么喚醒當前Condition對象中等待隊列的第一個結點的線程,并從等待隊列中移除該結點,移動到同步隊列中,如果加入同步隊列失敗,那么繼續循環喚醒等待隊列中的其他結點的線程,如果成功加入同步隊列,那么如果其前驅結點是否已結束或者設置前驅節點狀態為Node.SIGNAL狀態失敗,則通過LockSupport.unpark()喚醒被通知節點代表的線程,到此signal()任務完成,注意被喚醒后的線程,將從前面的await()方法中的while循環中退出,因為此時該線程的結點已在同步隊列中,那么while (!isOnSyncQueue(node))將不在符合循環條件,進而調用AQS的acquireQueued()方法加入獲取同步狀態的競爭中,這就是等待喚醒機制的整個流程實現原理,流程如下圖所示(注意無論是同步隊列還是等待隊列使用的Node數據結構都是同一個,不過是使用的內部變量不同罷了)。

ok~,本篇先到這,關于AQS中的另一種模式即共享模式,下篇再詳聊,歡迎繼續關注。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容