併發類Condition原始碼分析
一、用ReentrantLock加condition完成生產消費者模型
在前面學習 synchronized 的時候,有講到 wait/notify 的基本使用,結合 synchronized 可以實現對執行緒的通訊。那麼這個時候我就在思考了,既然 J.U.C 裡面提供了鎖的實現機制,那 J.U.C 裡面有沒有提供類似的執行緒通訊的工具呢? 於是找阿找,發現了一個Condition 工具類。Condition 是一個多執行緒協調通訊的工具類,可以讓某些執行緒一起等待某個條件(condition),只有滿足條件時,執行緒才會被喚醒。
condition基本使用:
生產者:
package com.gupao.condition;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class Product implements Runnable {
private Queue<Integer> queue;
private Lock lock;
private Condition condition;
private int maxSize;
public Product(Queue<Integer> queue, Lock lock, Condition condition, int maxSize) {
this.queue = queue;
this.lock = lock;
this.condition = condition;
this.maxSize = maxSize;
}
@Override
public void run() {
int i = 0;
while (true) {
i++;
lock.lock();
while (queue.size() == maxSize) {
try {
System.out.println("佇列滿了!");
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
queue.add(i);
System.out.println("生產者生產訊息" + i);
condition.signal();
lock.unlock();
}
}
}
消費者:
package com.gupao.condition;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class Customer implements Runnable {
// 對列
private Queue<Integer> queue;
// 鎖
private Lock lock;
private Condition condition;
// 佇列的大小
private int maxSize;
public Customer(Queue<Integer> queue, Lock lock, Condition condition, int maxSize) {
this.queue = queue;
this.lock = lock;
this.condition = condition;
this.maxSize = maxSize;
}
@Override
public void run() {
int i = 0;
while (true) {
i++;
lock.lock();
while (queue.size() == 0) {
try {
System.out.println("佇列空了!");
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Integer remove = queue.remove();
System.out.println("消費者消費訊息" + remove);
condition.signal();
lock.unlock();
}
}
}
測試類:
public class main {
public static void main(String[] args) throws InterruptedException {
Queue<Integer> queue = new LinkedBlockingQueue<>();
int maxSize = 5;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
Thread t1 = new Thread(new Product(queue,lock,condition,maxSize));
Thread t2 = new Thread(new Customer(queue, lock, condition, maxSize));
t2.start();
Thread.sleep(500);
t1.start();
}
}
二、原始碼分析
該方法是建立在已經拿到鎖的情況下。想了解lock的原理的可以看我另外一篇部落格
ReentrantLock原理分析
呼叫Condition,需要獲得 Lock 鎖,所以意味著會存在一個 AQS 同步佇列,在上面那個案例中,假如兩個執行緒同時執行的話,那麼 AQS 的佇列可能是下面這種情況
呼叫Condition 的await()方法(或者以 await 開頭的方法),會使當前執行緒進入等待佇列並釋放鎖,同時執行緒狀態變為等待狀態。
public final void await() throws InterruptedException {
// 該方法允許被中斷
if (Thread.interrupted())
throw new InterruptedException();
// 建立一個新的節點加入condition佇列中,節點狀態為condition
Node node = addConditionWaiter();
// 釋放當前的鎖,得到鎖的狀態,並喚醒 AQS 佇列中的一個執行緒
int savedState = fullyRelease(node);
int interruptMode = 0;
// 如果當前節點沒有在同步佇列上,即還沒有被 signal,則將當前執行緒阻塞
while (!isOnSyncQueue(node)) {
// 判斷這個節點是否在 AQS 佇列上,第一次判斷的是 false,因為前面已經釋放鎖了
// 通過 park 掛起當前執行緒
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 當這個執行緒醒來,會嘗試拿鎖, 當acquireQueued返回 false 就是拿到鎖了.
// interruptMode != THROW_IE -> 表示這個執行緒沒有成功將 node 入隊,但 signal 執行了 enq 方法讓其入隊了.
// 將這個變數設定成 REINTERRUPT.
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 如果 node 的下一個等待者不是 null, 則進行清理,清理 Condition 佇列上的節點.
// 如果是 null ,就沒有什麼好清理的了.
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
// 如果執行緒被中斷了,需要丟擲異常.或者什麼都不做
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
addConditionWaiter
這個方法的主要作用是把當前執行緒封裝成 Node,新增到等待佇列。這裡的佇列不再是雙向連結串列,而是單向連結串列
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
// 如果最後一個節點不等於condtion狀態,則從連結串列中刪除,不是condition狀態就是cancelled狀態
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;
}
unlinkCancelledWaiters
這個方法是清理掉連結串列中的狀態為cancelled狀態的節點
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null;
// 如果首節點不為空
while (t != null) {
// 獲取到下個節點
Node next = t.nextWaiter;
// 如果該節點的狀態不等於conditon,則該節點需要在連結串列中刪除
if (t.waitStatus != Node.CONDITION) {
// 該節點的下個節點設定為空,意味著垃圾回收後就回收該節點
t.nextWaiter = null;
// trail 為空,則把下一個節點負責給首節點
if (trail == null)
firstWaiter = next;
else
// 把下一個節點賦值給next,這樣連結串列就要繼續連線起來
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
// 等於condtion,把該節點賦值給尾節點
else
trail = t;
// 下個一個節點賦值給t,進行下一次迴圈
t = next;
}
}
圖解分析
執行完addConditionWaiter 這個方法之後,就會產生一個這樣的condition 佇列
fullyRelease
fullRelease,就是徹底的釋放鎖,什麼叫徹底呢,就是如果當前鎖存在多次重入,那麼在這個方法中只需要釋放一次就會把所有的重入次數歸零。
final int fullyRelease(Node node) {
boolean failed = true;
try {
// 獲得重入鎖的狀態
int savedState = getState();
// 釋放鎖,並且喚醒同步佇列中的下一個執行緒
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
圖解分析
此時,同步佇列會觸發鎖的釋放和重新競爭。ThreadB 獲得了鎖。
isOnSyncQueue
判斷當前節點是否在同步佇列中,返回 false 表示不在,返回true 表示如果不在,
講當前執行緒掛起
final boolean isOnSyncQueue(Node node) {
// 如果狀態是condition,證明一定不再同步佇列裡,condition狀態只存在於等待佇列,在同步佇列裡,node.prev是一定不為空的,因為有個head的節點
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
// 在等待佇列裡,node.next 是等於空的,不等於空就是在同步佇列當中
if (node.next != null) // If has successor, it must be on queue
return true;
// 遍歷正個同步佇列,判斷node是否在同步佇列當中
return findNodeFromTail(node);
}
await 方法會阻塞 ThreadA,然後 ThreadB 搶佔到了鎖獲得了執行許可權,這個時候在 ThreadB 中呼叫了 Condition 的 signal()方法,將會喚醒在等待佇列中節點
public final void signal() {
// 判斷當前執行緒是否獲取到了鎖,如果沒有拋異常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
// 如果首節點不為空,喚醒首節點
if (first != null)
doSignal(first);
}
doSignal
private void doSignal(Node first) {
do {
// frist的下一個節點如果為空,就把lastWaiter設定為空
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
// 不為空,再把first 節點從等待佇列中移除
first.nextWaiter = null;
} while (!transferForSignal(first) &&
// 返回false,firstWaiter已經被從新賦值過了,如果不是空,進行下一次遍歷
(first = firstWaiter) != null);
}
transferForSignal
final boolean transferForSignal(Node node) {
// 如果該節點不是condition狀態(可能程式設計了cancelled狀態),waitStatus=0,就會設定失敗,返回false
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 將該節點放入到AQS佇列中,返回上一個節點
Node p = enq(node);
int ws = p.waitStatus;
// 如果上一個節點狀態沒有被改變,也就是沒有程式設計cancelled狀態,就將該節點狀態設定成singal狀態
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// 如果狀態已經是cancelled狀態,將該節點的執行緒掛起
LockSupport.unpark(node.thread);
return true;
}
被阻塞的執行緒喚醒後的邏輯
前面在分析await 方法時,執行緒會被阻塞。而通過 signal被喚醒之後又繼續回到上次執行的邏輯中
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
checkInterruptWhileWaiting
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
transferAfterCancelledWait
進入到該方法,證明該執行緒被中斷過。
這裡有個知識點,就是進入該執行緒進入該方法,不一定是被signal喚醒點,也有可能是是呼叫了該執行緒的interrupt()方法,這個方法會更新一箇中斷標識,並且會喚醒處於阻塞狀態下的執行緒。
final boolean transferAfterCancelledWait(Node node) {
// 如果cas成功,代表是被執行緒中斷的,放入到AQS佇列,返回true
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
enq(node);
return true;
}
// 如果cas 失敗,返回false
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
如果返回true,證明這個沒有執行被signal喚醒,就被中斷了,checkInterruptWhileWaiting返回THROW_IE
如果返回false, 證明這個這個方法是被喚醒以後,在被中斷的
checkInterruptWhileWaiting返回REINTERRUPT
reportInterruptAfterWait
如果是THROW_IE,則丟擲中斷異常
如果是REINTERRUPT,則重新響應中斷
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
// 如果是被中斷喚醒的,執行緒直接拋異常
if (interruptMode == THROW_IE)
throw new InterruptedException();
// 如果是被
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
相關文章
- 併發程式設計之 Condition 原始碼分析程式設計原始碼
- 07 併發工具類CountDownLatch、CyclicBarrier、Semaphore使用及原始碼分析CountDownLatch原始碼
- Java併發——ConditionJava
- 02.併發程式設計(2)Thread類原始碼分析程式設計thread原始碼
- 併發工具類:Semaphore原始碼解讀原始碼
- 原始碼|併發一枝花之ReentrantLock與AQS(3):Condition原始碼ReentrantLockAQS
- 併發-10-Condition
- 併發程式設計—— FutureTask 原始碼分析程式設計原始碼
- Java併發之AQS原始碼分析(二)JavaAQS原始碼
- 併發系列(二)——FutureTask類原始碼簡析原始碼
- 併發程式設計之 Exchanger 原始碼分析程式設計原始碼
- java 併發程式設計-AQS原始碼分析Java程式設計AQS原始碼
- 併發程式設計之 CountDown 原始碼分析程式設計原始碼
- 併發程式設計之 CyclicBarrier 原始碼分析程式設計原始碼
- Java併發程式設計 -- ConditionJava程式設計
- Java 併發程式設計(十五) -- Semaphore原始碼分析Java程式設計原始碼
- Java 併發程式設計(十四) -- CyclicBarrier原始碼分析Java程式設計原始碼
- Java 併發程式設計(十三) -- CountDownLatch原始碼分析Java程式設計CountDownLatch原始碼
- Java 併發程式設計(七) -- AbstractQueuedSynchronizer 原始碼分析Java程式設計原始碼
- 併發程式設計 —— Timer 原始碼分析程式設計原始碼
- 併發程式設計之 SynchronousQueue 核心原始碼分析程式設計原始碼
- 併發程式設計之——寫鎖原始碼分析程式設計原始碼
- Java 併發程式設計(九) -- ReentrantLock 原始碼分析Java程式設計ReentrantLock原始碼
- 死磕 java併發包之AtomicInteger原始碼分析Java原始碼
- Java併發集合類ConcurrentHashMap底層核心原始碼解析JavaHashMap原始碼
- 學習JUC原始碼(3)——Condition等待佇列(原始碼分析結合圖文理解)原始碼佇列
- Java 併發程式設計之 Condition 介面Java程式設計
- Java併發包原始碼學習系列:詳解Condition條件佇列、signal和awaitJava原始碼佇列AI
- 詳解Java 容器(第⑤篇)——容器原始碼分析 - 併發容器Java原始碼
- 死磕 java併發包之LongAdder原始碼分析Java原始碼
- 精盡Spring Boot原始碼分析 - Condition 介面的擴充套件Spring Boot原始碼套件
- Laravel 請求類原始碼分析Laravel原始碼
- JDK 原始碼分析(1) Object類JDK原始碼Object
- DRF之排序類原始碼分析排序原始碼
- DRF之分頁類原始碼分析原始碼
- java併發之hashmap原始碼JavaHashMap原始碼
- 併發系列(一)——執行緒池原始碼(ThreadPoolExecutor類)簡析執行緒原始碼thread
- 併發程式設計 —— 原始碼分析公平鎖和非公平鎖程式設計原始碼