源碼分析:②ReentrantLock之條件鎖Condition

簡介

條件鎖塔沃,指在獲得鎖之后阵翎,還需要達成某些條件后院领,才能繼續(xù)執(zhí)行的鎖双絮。且必須配合Lock一起使用浴麻,也就是說必須獲得鎖之后才可以調(diào)用condition.await()方法

源碼分析

ReentrantLock 的條件鎖使用的 AbstractQueuedSynchronizer 中的ConditionObject 來實現(xiàn)的,所以其實標題說的ReentrantLock 源碼分析囤攀,其實應該是AQS源碼分析之條件鎖Condition软免,但是這里為什么還是要說成ReentrantLock 源碼分析呢?主要是AQS是一個抽象類焚挠,用戶并不能直接使用膏萧,而ReentrantLock 提供了使用條件鎖的入口,源碼如下::

final ConditionObject newCondition() {
    return new ConditionObject();
}

Condition 接口

Condition 是一個接口蝌衔,定義了7個方法榛泛,分別是:

  1. void await() throws InterruptedException;
    使當前線程等待,直到發(fā)出信號或中斷
  2. boolean await(long time, TimeUnit unit) throws InterruptedException;
    使當前線程等待噩斟,直到它被喚醒或中斷曹锨,或指定的等待時間被終止。等價于:awaitNanos(unit.toNanos(time)) > 0
  3. long awaitNanos(long nanosTimeout) throws InterruptedException;
    使當前線程等待剃允,直到發(fā)出信號或中斷拂檩,或過去指定的等待時間
  4. void awaitUninterruptibly();
    使當前線程等待,直到發(fā)出信號為止
  5. boolean awaitUntil(Date deadline) throws InterruptedException;
    使當前線程等待幻赚,直到發(fā)出信號或中斷靶橱,或過去指定的截止時間
  6. void signal();
    喚醒一個等待的線程
  7. void signalAll();
    喚醒所有等待的線程

總結下來,就是await营袜、signal撒顿、signalAll,所以下面我們也主要分析這三個方法荚板。

AQS.ConditionObject類

ConditionObject 是AQS是的一個內(nèi)部類凤壁,實現(xiàn)了Condition 接口,并且實現(xiàn)它的全部方法跪另,ConditionObject 也維護了一個隊列拧抖,為了和AbstractQueuedSynchronizer內(nèi)部類Node組成的隊列區(qū)分開,這里的隊列我們下面稱為等待隊列免绿,Node組成的隊列稱為同步隊列唧席,等待隊列源碼如下:

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;
}

Condition.await()方法

使當前線程等待,直到發(fā)出信號或中斷嘲驾,如果當前線程被中斷淌哟,拋出InterruptedException

源碼解析:

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        // 如果當前線程被中斷,拋出InterruptedException
        throw new InterruptedException();
    // 以當前線程為節(jié)點添加到等待隊列辽故,并返回當前節(jié)點
    Node node = addConditionWaiter();
    // 完全釋放當前線程獲得鎖徒仓,并返回釋放前state的值
    int savedState = fullyRelease(node);
    // 中斷標識
    int interruptMode = 0;
    // 檢查當前節(jié)點的是否在同步隊列,注意前面的感嘆號誊垢,是節(jié)點不在同步隊列中掉弛,才將當前線程park
    while (!isOnSyncQueue(node)) {
        // 調(diào)用Unsafa類底層阻塞線程,等待喚醒自己的條件信號
        LockSupport.park(this);
        // 當被喚醒以后喂走,接著從下面開始執(zhí)行
        // checkInterruptWhileWaiting 檢查線程是否被中斷
        // 發(fā)出信號之前被中斷殃饿,返回-1,發(fā)出信號之后被中斷缴啡,返回1壁晒,沒有被中斷,返回0
        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)
        // 發(fā)生了中斷碘裕,拋出異承。或者重新中斷當前線程
        reportInterruptAfterWait(interruptMode);
}

await()方法過程總結:

  1. 檢查線程中斷情況,如果當前線程被中斷帮孔,拋出InterruptedException
  2. 以當前線程為節(jié)點添加到等待隊列雷滋,并返回當前節(jié)點
  3. 完全釋放當前線程獲得鎖不撑,并返回釋放前 state 的值
  4. 檢查當前節(jié)點的是否在同步隊列
    • 不在同步隊列,調(diào)用Unsafa類底層 park 阻塞線程晤斩,等待喚醒信號
  5. 當被喚醒以后焕檬,再次從同步隊列獲得鎖,獲取不到鎖會再次阻塞線程
  6. 判斷條件等待隊列中有沒有線程被取消澳泵,如果有实愚,則將它們清除
  7. 如果發(fā)生了中斷,拋出異惩酶ǎ或者重新中斷當前線程

Condition.signal()方法

“喚醒”一個等待時間最長的線程腊敲,也就是等待隊列的第一個線程——firstWaiter;

源碼解析:

public final void signal() {
    // 判斷是否是當前線程持有鎖维苔,不是則拋出異常
    // 說明了調(diào)用這個方法之前也必須要持有鎖
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 等待隊列隊頭碰辅,理論上就是第一次調(diào)用await()時加入的節(jié)點線程
    Node first = firstWaiter;
    if (first != null)
        // 發(fā)信號
        doSignal(first);
}

private void doSignal(Node first) {
    do {
        // firstWaiter = first.nextWaiter   重新賦值等待隊列頭結點
        if ( (firstWaiter = first.nextWaiter) == null)
            // 等待隊列 為空
            lastWaiter = null;
        // 斷掉節(jié)點關系
        first.nextWaiter = null;
       // transferForSignal 將節(jié)點從等待隊列轉(zhuǎn)移到同步隊列
    } while (!transferForSignal(first) && (first = firstWaiter) != null);
}
//  node節(jié)點是等待隊列上的節(jié)點
final boolean transferForSignal(Node node) {
    // 改變節(jié)點的等待狀態(tài)為0
    // 0表示:當前節(jié)點在sync隊列中,等待著獲取鎖介时。-2表示當前節(jié)點在等待condition没宾,也就是在condition隊列中
    // 返回false,外層的循環(huán)繼續(xù)執(zhí)行
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    // 將節(jié)點加入到同步隊列中,返回node節(jié)點的前驅(qū)結點潮尝,也就是老的尾節(jié)點
    Node p = enq(node);
    int ws = p.waitStatus;
    // 大于0的狀態(tài)只有1榕吼,也就是取消
    // 如果老的尾節(jié)點被取消 或者 更新老的尾節(jié)點為SIGNAL失敗,可以直接輪到當前節(jié)點勉失,直接喚醒當前節(jié)點的線程
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    // 如果老的尾節(jié)點沒有被取消 或者 更新老的尾節(jié)點為SIGNAL成功羹蚣,則返回true
    // 返回true的話,外層的do循環(huán)會直接退出
    // 所以這個方法最核心的邏輯知識把等待隊列的節(jié)點轉(zhuǎn)移到同步隊列中去
    // 轉(zhuǎn)移到同步隊列后乱凿,signal()方法調(diào)用完成后緊接著應該是unlock()方法顽素,移動同步隊列的新節(jié)點等待被喚醒
    return true;
}

signal()方法過程總結:

  1. 判斷是否是當前線程持有鎖在調(diào)用signal方法,不是則拋出異常(調(diào)用signal()方法之前必須調(diào)用lock()方法)
  2. 拿到等待隊列隊頭節(jié)點 firstWaiter徒蟆,理論上就是第一次調(diào)用await()時加入的節(jié)點胁出,節(jié)點存在則繼續(xù)調(diào)用dosignal()。
  3. 先使用 CAS 方式修改節(jié)點的waitStatus的值為0全蝶,表示此節(jié)點在同步隊列中
  4. 將節(jié)點加入到同步隊列中(enq(node)),返回node節(jié)點的前驅(qū)結點寺枉,也就是老的尾節(jié)點
  5. 同步隊列中抑淫,如果老的尾節(jié)點被取消 或者 更新老的尾節(jié)點為SIGNAL失敗
    說明可以直接輪到當前節(jié)點,直接喚醒等待隊列第一個節(jié)點的線程
  6. 如果老的尾節(jié)點沒有被取消 或者 更新老的尾節(jié)點為SIGNAL成功姥闪,則返回true始苇,返回true的話,外層的do循環(huán)會直接退出筐喳,結束signal()方法催式。

最后如果直接返回true函喉,第5步?jīng)]有執(zhí)行,那signal()方法就沒有地方調(diào)用了unpark方法了荣月,那線程是在什么時候被喚醒的呢管呵?
signal()方法核心任務只是把等待隊列中的節(jié)點轉(zhuǎn)移到同步隊列中,signal()方法執(zhí)行完成后喉童,理論上會執(zhí)行后面的unlock()方法撇寞,tryRelease()解鎖成功會調(diào)用unparkSuccessor(node)方法,執(zhí)行LockSupport.unpark(thread)堂氯,同步隊列中的(等待)節(jié)點線程被喚醒,繼續(xù)執(zhí)行await()方法之后的代碼牌废。

Condition.signalAll()方法

signalAll 和 signal 方法很相似咽白,signal方法在doSignal的時候只是把等到隊列的第一個節(jié)點轉(zhuǎn)移到同步隊列,而signalAll是遍歷等待隊列鸟缕,把隊列中所有節(jié)點都轉(zhuǎn)移到同步隊列中去

源碼展示:

private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    // 遍歷所有的等待隊列
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        // 等待隊列轉(zhuǎn)移到同步隊列晶框,signal方法也是同樣轉(zhuǎn)移的
        transferForSignal(first);
        first = next;
    } while (first != null);
}

經(jīng)典用例ArrayBlockingQueue:

比如最典型的阻塞隊列 ArrayBlockingQueue,當隊列中沒有元素的時候懂从,他無法take出一個元素授段,需要等待其他線程入隊一個元素后喚醒它,才能繼續(xù)彈出元素番甩。

它有三個重要的屬性侵贵,一個鎖和兩個條件,源碼如下:

final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;

在構造方法中初始化:

public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}

take() 方法:

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            **notEmpty.await();**
        return dequeue();
    } finally {
        lock.unlock();
    }
}

enqueue(E)方法:

private void enqueue(E x) {
    // assert lock.getHoldCount() == 1;
    // assert items[putIndex] == null;
    final Object[] items = this.items;
    items[putIndex] = x;
    if (++putIndex == items.length)
        putIndex = 0;
    count++;
    **notEmpty.signal();**
}

從上面take方法可以看出缘薛,當隊列為空時窍育,線程要等待入隊發(fā)生,而不是直接出隊返回宴胧;

當入隊方法enqueue調(diào)用時漱抓,隊列不為空,notEmpty.signal() 喚醒等待的線程恕齐。

put(E)方法:

public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length)
            **notFull.await();**
        enqueue(e);
    } finally {
        lock.unlock();
    }
}

插入元素的時候乞娄,如果隊列已經(jīng)滿了,線程要等待显歧,等待隊列不是滿的狀態(tài)時才可以執(zhí)行后面的入隊操作仪或;

出隊或remove等操作之后,會觸發(fā)喚醒等待的線程:

private E dequeue() {
    // assert lock.getHoldCount() == 1;
    // assert items[takeIndex] != null;
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    E x = (E) items[takeIndex];
    items[takeIndex] = null;
    if (++takeIndex == items.length)
        takeIndex = 0;
    count--;
    if (itrs != null)
        itrs.elementDequeued();
    **notFull.signal();**
    return x;
}

注意追迟,signal 和 await 要成對調(diào)用溶其,不然只調(diào)用 await 動作,線程則會一直等待敦间,除非線程被中斷瓶逃。

Condition 總結

  1. Reentranlock 的條件鎖是基于AQS框架中的ConditionObject來實現(xiàn)的束铭,自己一句代碼都沒有寫,都是用的它爸爸的代碼厢绝。
  2. 從源碼也可以看出契沫,使用條件鎖的當前線程必須持有鎖,代碼上表示也就是使用Condition.await()時必須要lock.lock()
  3. await() 方法首先會完全釋放當前線程獲得的鎖昔汉,然后再把當前線程的節(jié)點加入到等待隊列中懈万,然后調(diào)用Unsafa類底層 park 阻塞線程,等待被喚醒
  4. signal() 方法核心是就是把等待隊列中的一個節(jié)點轉(zhuǎn)移到同步隊列中靶病,不一定會馬上喚醒線程
  5. signalAll() 方法核心是就是把等待隊列中的所有節(jié)點轉(zhuǎn)移到同步隊列中会通,不一定會馬上喚醒線程

條件鎖使用的簡單流程總結

  1. A線程 獲得鎖 lock
  2. A線程 await
    1. A線程釋放鎖
    2. A線程加入到等待隊列
    3. A線程阻塞 park
  3. B線程 獲得鎖 lock
  4. B線程 signal
    1. B線程 把等待隊列中的A線程轉(zhuǎn)移到同步隊列
  5. B線程 釋放鎖 unlock
  6. A線程被喚醒 unpark
  7. A線程 繼續(xù)執(zhí)行await方法后面的代碼
  8. A線程釋放鎖 unlock
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市娄周,隨后出現(xiàn)的幾起案子涕侈,更是在濱河造成了極大的恐慌,老刑警劉巖煤辨,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件裳涛,死亡現(xiàn)場離奇詭異,居然都是意外死亡众辨,警方通過查閱死者的電腦和手機端三,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鹃彻,“玉大人郊闯,你說我怎么就攤上這事「∩” “怎么了虚婿?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長泳挥。 經(jīng)常有香客問我然痊,道長,這世上最難降的妖魔是什么屉符? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任剧浸,我火速辦了婚禮,結果婚禮上矗钟,老公的妹妹穿的比我還像新娘唆香。我一直安慰自己,他們只是感情好吨艇,可當我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布躬它。 她就那樣靜靜地躺著,像睡著了一般东涡。 火紅的嫁衣襯著肌膚如雪冯吓。 梳的紋絲不亂的頭發(fā)上倘待,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天,我揣著相機與錄音组贺,去河邊找鬼凸舵。 笑死,一個胖子當著我的面吹牛失尖,可吹牛的內(nèi)容都是我干的啊奄。 我是一名探鬼主播,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼掀潮,長吁一口氣:“原來是場噩夢啊……” “哼菇夸!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起仪吧,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤峻仇,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后邑商,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡凡蚜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年人断,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片朝蜘。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡恶迈,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出谱醇,到底是詐尸還是另有隱情暇仲,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布副渴,位于F島的核電站奈附,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏煮剧。R本人自食惡果不足惜斥滤,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望勉盅。 院中可真熱鬧佑颇,春花似錦、人聲如沸草娜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宰闰。三九已至茬贵,卻和暖如春簿透,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背闷沥。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工萎战, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人舆逃。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓蚂维,卻偏偏與公主長得像,于是被迫代替她去往敵國和親路狮。 傳聞我的和親對象是個殘疾皇子虫啥,可洞房花燭夜當晚...
    茶點故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內(nèi)容