圖解Java?ReentrantLock的條件變量Condition機制
概述
想必大家都使用過wait()和notify()這兩個方法把,這兩個方法主要用于多線程間的協(xié)同處理,即控制線程之間的等待、通知、切換及喚醒。而RenentrantLock也支持這樣條件變量的能力,而且相對于synchronized 更加強大,能夠支持多個條件變量。
最好可以先閱讀ReentrantLock系列文章:
圖解Java ReentrantLock公平鎖和非公平鎖的實現(xiàn)
詳解Java ReentrantLock可重入,可打斷,鎖超時的實現(xiàn)原理
ReentrantLock條件變量使用
ReentrantLock類API
Condition newCondition()
: 創(chuàng)建條件變量對象
Condition類API
void await()
: 當(dāng)前線程從運行狀態(tài)進入等待狀態(tài),同時釋放鎖,該方法可以被中斷void awaitUninterruptibly()
:當(dāng)前線程從運行狀態(tài)進入等待狀態(tài),該方法不能夠被中斷void signal()
: 喚醒一個等待在 Condition 條件隊列上的線程void signalAll()
: 喚醒阻塞在條件隊列上的所有線程
@Test public void testCondition() throws InterruptedException { ReentrantLock lock = new ReentrantLock(); //創(chuàng)建新的條件變量 Condition condition = lock.newCondition(); Thread thread0 = new Thread(() -> { lock.lock(); try { System.out.println("線程0獲取鎖"); // sleep不會釋放鎖 Thread.sleep(500); //進入休息室等待 System.out.println("線程0釋放鎖,進入等待"); condition.await(); System.out.println("線程0被喚醒了"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }); thread0.start(); //叫醒 Thread thread1 = new Thread(() -> { lock.lock(); try { System.out.println("線程1獲取鎖"); //喚醒 condition.signal(); System.out.println("線程1喚醒線程0"); } finally { lock.unlock(); System.out.println("線程1釋放鎖"); } }); thread1.start(); thread0.join(); thread1.join(); }
運行結(jié)果:
- condition的wait和notify必須在lock范圍內(nèi)
- 實現(xiàn)條件變量的等待和喚醒,他們必須是同一個condition。
- 線程1執(zhí)行conidtion.notify()后,并沒有釋放鎖,需要等釋放鎖后,線程0重新獲取鎖成功后,才能繼續(xù)向下執(zhí)行。
圖解實現(xiàn)原理
await過程
1.線程0(Thread-0)一開始獲取鎖,exclusiveOwnerThread字段是Thread-0, 如下圖中的深藍色節(jié)點
2.Thread-0調(diào)用await方法,Thread-0封裝成Node進入ConditionObject的隊列,因為此時只有一個節(jié)點,所有firstWaiter和lastWaiter都指向Thread-0,會釋放鎖資源,NofairSync中的state會變成0,同時exclusiveOwnerThread設(shè)置為null。如下圖所示。
3.線程1(Thread-1)被喚醒,重新獲取鎖,如下圖的深藍色節(jié)點所示。
4.Thread-0被park阻塞,如下圖灰色節(jié)點所示:
源碼如下:
下面是await()方法的整體流程,其中LockSupport.park(this)
進行阻塞當(dāng)前線程,后續(xù)喚醒,也會在這個程序點恢復(fù)執(zhí)行。
public final void await() throws InterruptedException { // 判斷當(dāng)前線程是否是中斷狀態(tài),是就直接給個中斷異常 if (Thread.interrupted()) throw new InterruptedException(); // 將調(diào)用 await 的線程包裝成 Node,添加到條件隊列并返回 Node node = addConditionWaiter(); // 完全釋放節(jié)點持有的鎖,因為其他線程喚醒當(dāng)前線程的前提是【持有鎖】 int savedState = fullyRelease(node); // 設(shè)置打斷模式為沒有被打斷,狀態(tài)碼為 0 int interruptMode = 0; // 如果該節(jié)點還沒有轉(zhuǎn)移至 AQS 阻塞隊列, park 阻塞,等待進入阻塞隊列 while (!isOnSyncQueue(node)) { // 阻塞當(dāng)前線程,待會 LockSupport.park(this); // 如果被打斷,退出等待隊列,對應(yīng)的 node 【也會被遷移到阻塞隊列】尾部,狀態(tài)設(shè)置為 0 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } // 邏輯到這說明當(dāng)前線程退出等待隊列,進入【阻塞隊列】 // 嘗試槍鎖,釋放了多少鎖就【重新獲取多少鎖】,獲取鎖成功判斷打斷模式 if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; // node 在條件隊列時 如果被外部線程中斷喚醒,會加入到阻塞隊列,但是并未設(shè) nextWaiter = null if (node.nextWaiter != null) // 清理條件隊列內(nèi)所有已取消的 Node unlinkCancelledWaiters(); // 條件成立說明掛起期間發(fā)生過中斷 if (interruptMode != 0) // 應(yīng)用打斷模式 reportInterruptAfterWait(interruptMode); }
將線程封裝成Node, 加入到ConditionObject隊列尾部,此時節(jié)點的等待狀態(tài)時-2。
private Node addConditionWaiter() { // 獲取當(dāng)前條件隊列的尾節(jié)點的引用,保存到局部變量 t 中 Node t = lastWaiter; // 當(dāng)前隊列中不是空,并且節(jié)點的狀態(tài)不是 CONDITION(-2),說明當(dāng)前節(jié)點發(fā)生了中斷 if (t != null && t.waitStatus != Node.CONDITION) { // 清理條件隊列內(nèi)所有已取消的 Node unlinkCancelledWaiters(); // 清理完成重新獲取 尾節(jié)點 的引用 t = lastWaiter; } // 創(chuàng)建一個關(guān)聯(lián)當(dāng)前線程的新 node, 設(shè)置狀態(tài)為 CONDITION(-2),添加至隊列尾部 Node node = new Node(Thread.currentThread(), Node.CONDITION); if (t == null) firstWaiter = node; // 空隊列直接放在隊首【不用CAS因為執(zhí)行線程是持鎖線程,并發(fā)安全】 else t.nextWaiter = node; // 非空隊列隊尾追加 lastWaiter = node; // 更新隊尾的引用 return node; }
清理條件隊列中的cancel類型的節(jié)點,比如中斷、超時等會導(dǎo)致節(jié)點轉(zhuǎn)換為Cancel
// 清理條件隊列內(nèi)所有已取消(不是CONDITION)的 node,【鏈表刪除的邏輯】 private void unlinkCancelledWaiters() { // 從頭節(jié)點開始遍歷【FIFO】 Node t = firstWaiter; // 指向正常的 CONDITION 節(jié)點 Node trail = null; // 等待隊列不空 while (t != null) { // 獲取當(dāng)前節(jié)點的后繼節(jié)點 Node next = t.nextWaiter; // 判斷 t 節(jié)點是不是 CONDITION 節(jié)點,條件隊列內(nèi)不是 CONDITION 就不是正常的 if (t.waitStatus != Node.CONDITION) { // 不是正常節(jié)點,需要 t 與下一個節(jié)點斷開 t.nextWaiter = null; // 條件成立說明遍歷到的節(jié)點還未碰到過正常節(jié)點 if (trail == null) // 更新 firstWaiter 指針為下個節(jié)點 firstWaiter = next; else // 讓上一個正常節(jié)點指向 當(dāng)前取消節(jié)點的 下一個節(jié)點,【刪除非正常的節(jié)點】 trail.nextWaiter = next; // t 是尾節(jié)點了,更新 lastWaiter 指向最后一個正常節(jié)點 if (next == null) lastWaiter = trail; } else { // trail 指向的是正常節(jié)點 trail = t; } // 把 t.next 賦值給 t,循環(huán)遍歷 t = next; } }
fullyRelease方法將r讓Thread-0釋放鎖, 這個時候Thread-1就會去競爭鎖
// 線程可能重入,需要將 state 全部釋放 final int fullyRelease(Node node) { // 完全釋放鎖是否成功,false 代表成功 boolean failed = true; try { // 獲取當(dāng)前線程所持有的 state 值總數(shù) int savedState = getState(); // release -> tryRelease 解鎖重入鎖 if (release(savedState)) { // 釋放成功 failed = false; // 返回解鎖的深度 return savedState; } else { // 解鎖失敗拋出異常 throw new IllegalMonitorStateException(); } } finally { // 沒有釋放成功,將當(dāng)前 node 設(shè)置為取消狀態(tài) if (failed) node.waitStatus = Node.CANCELLED; } }
判斷節(jié)點是否在AQS阻塞對列中,不在條件對列中
final boolean isOnSyncQueue(Node node) { // node 的狀態(tài)是 CONDITION,signal 方法是先修改狀態(tài)再遷移,所以前驅(qū)節(jié)點為空證明還【沒有完成遷移】 if (node.waitStatus == Node.CONDITION || node.prev == null) return false; // 說明當(dāng)前節(jié)點已經(jīng)成功入隊到阻塞隊列,且當(dāng)前節(jié)點后面已經(jīng)有其它 node,因為條件隊列的 next 指針為 null if (node.next != null) return true; // 說明【可能在阻塞隊列,但是是尾節(jié)點】 // 從阻塞隊列的尾節(jié)點開始向前【遍歷查找 node】,如果查找到返回 true,查找不到返回 false return findNodeFromTail(node); }
signal過程
1.Thread-1執(zhí)行signal方法喚醒條件隊列中的第一個節(jié)點,即Thread-0,條件隊列置空
2.Thread-0的節(jié)點的等待狀態(tài)變更為0, 重新加入到AQS隊列尾部。
3.后續(xù)就是Thread-1釋放鎖,其他線程重新?lián)屾i。
源碼如下:
signal()方法是喚醒的入口方法
public final void signal() { // 判斷調(diào)用 signal 方法的線程是否是獨占鎖持有線程 if (!isHeldExclusively()) throw new IllegalMonitorStateException(); // 獲取條件隊列中第一個 Node Node first = firstWaiter; // 不為空就將第該節(jié)點【遷移到阻塞隊列】 if (first != null) doSignal(first); }
調(diào)用doSignal()方法喚醒節(jié)點
// 喚醒 - 【將沒取消的第一個節(jié)點轉(zhuǎn)移至 AQS 隊列尾部】 private void doSignal(Node first) { do { // 成立說明當(dāng)前節(jié)點的下一個節(jié)點是 null,當(dāng)前節(jié)點是尾節(jié)點了,隊列中只有當(dāng)前一個節(jié)點了 if ((firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; // 將等待隊列中的 Node 轉(zhuǎn)移至 AQS 隊列,不成功且還有節(jié)點則繼續(xù)循環(huán) } while (!transferForSignal(first) && (first = firstWaiter) != null); } // signalAll() 會調(diào)用這個函數(shù),喚醒所有的節(jié)點 private void doSignalAll(Node first) { lastWaiter = firstWaiter = null; do { Node next = first.nextWaiter; first.nextWaiter = null; transferForSignal(first); first = next; // 喚醒所有的節(jié)點,都放到阻塞隊列中 } while (first != null); }
調(diào)用transferForSignal()方法,先將節(jié)點的 waitStatus 改為 0,然后加入 AQS 阻塞隊列尾部,將 Thread-3 的 waitStatus 改為 -1。
// 如果節(jié)點狀態(tài)是取消, 返回 false 表示轉(zhuǎn)移失敗, 否則轉(zhuǎn)移成功 final boolean transferForSignal(Node node) { // CAS 修改當(dāng)前節(jié)點的狀態(tài),修改為 0,因為當(dāng)前節(jié)點馬上要遷移到阻塞隊列了 // 如果狀態(tài)已經(jīng)不是 CONDITION, 說明線程被取消(await 釋放全部鎖失?。┗蛘弑恢袛啵纱驍?cancelAcquire) if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) // 返回函數(shù)調(diào)用處繼續(xù)尋找下一個節(jié)點 return false; // 【先改狀態(tài),再進行遷移】 // 將當(dāng)前 node 入阻塞隊列,p 是當(dāng)前節(jié)點在阻塞隊列的【前驅(qū)節(jié)點】 Node p = enq(node); int ws = p.waitStatus; // 如果前驅(qū)節(jié)點被取消或者不能設(shè)置狀態(tài)為 Node.SIGNAL,就 unpark 取消當(dāng)前節(jié)點線程的阻塞狀態(tài), // 讓 thread-0 線程競爭鎖,重新同步狀態(tài) if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) LockSupport.unpark(node.thread); return true; }
以上就是圖解Java ReentrantLock的條件變量Condition機制的詳細內(nèi)容,更多關(guān)于ReentrantLock條件變量Condition機制的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解Spring Boot 使用slf4j+logback記錄日志配置
本篇文章主要介紹了Spring Boot 使用slf4j+logback記錄日志配置,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-05-05spring security 5.x實現(xiàn)兼容多種密碼的加密方式
spring security針對該功能有兩種實現(xiàn)方式,一種是簡單的使用加密來保證基于 cookie 的 token 的安全,另一種是通過數(shù)據(jù)庫或其它持久化存儲機制來保存生成的 token。這篇文章主要給大家介紹了關(guān)于spring security 5.x實現(xiàn)兼容多種密碼的加密方式,需要的朋友可以參考下。2018-01-01SSH框架網(wǎng)上商城項目第12戰(zhàn)之添加和更新商品功能
這篇文章主要介紹了SSH框架網(wǎng)上商城項目第12戰(zhàn)之添加和更新商品功能的實現(xiàn)代碼,感興趣的小伙伴們可以參考一下2016-06-06Java ArrayList與LinkedList及HashMap容器的用法區(qū)別
這篇文章主要介紹了Java ArrayList與LinkedList及HashMap容器的用法區(qū)別,具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-07-07