フォロー歓迎: Wang Youzhi
、バケツを運ぶ Java 人々のグループに参加することを楽しみにしています:一緒に裕福な Java 人々
今すぐチャットに来てくださいCondition
。AQS Condition
"ファミリー" に待機およびウェイクアップ機能を提供し、AQS "ファミリー" がsynchronized
スレッドと同様にスレッドを一時停止およびウェイクアップできるようにします。まず、Condition
面接での次の 2 つの質問を見てみましょう。
Condition
wait と wake forObject
はどう違いますか?Condition
キューとは何ですか?
次に、「何を」「どう使うか」「どう実装するか」の順にベールを脱いでいきましょうCondition
。
条件とは何ですか?
Condition
Object#wait
は、および とObject#notify
同じ機能を提供する Java のインターフェイスです。Doug Lea はCondition
インターフェイスの説明で次のように述べています。
条件 (条件キューまたは条件変数とも呼ばれる) は、ある状態条件が true になる可能性があることを別のスレッドから通知されるまで、あるスレッドが実行を一時停止する (「待機」する) 手段を提供します。
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();
}
Condition
提供されている関数はawait (await) と wakeup (signal)の 2 つだけです。これらはObject
提供されている wait と wakeup に似ています。
public final void wait() throws InterruptedException;
public final void wait(long timeoutMillis, int nanos) throws InterruptedException;
public final native void wait(long timeoutMillis) throws InterruptedException;
@HotSpotIntrinsicCandidate
public final native void notify();
@HotSpotIntrinsicCandidate
public final native void notifyAll();
ウェイクアップ機能に関しては、次のものCondition
と大きな違いはありませんObject
。
Condition#signal
≈ \約≈Object#notify
Condition#signalAll
= ==Object#notifyAll
複数のスレッドが待機状態にある場合、Object#notify()
スレッドは「ランダムに」ウェイクアップされ、Condition#signal
スレッドをウェイクアップする方法は特定の実装によって決定されます。たとえば、ConditionObject
待機状態に入った最初のスレッドがウェイクアップされますが、両方のメソッドはウェイクアップするだけですスレッドを 1 つ上げます。
待機関数に関して、Condition
と の共通点Object
は、保持されているリソースを解放し、Condition
ロックを解放し、Object
モニターを解放します。つまり、待機状態に入った後、他のスレッドがロック/モニターを取得できるようになります。主な違いは、Condition
より豊富なシナリオのサポートに反映されており、表で比較できます。
Condition 方法 |
Object 方法 |
説明 |
---|---|---|
Condition#await() |
Object#wait() |
スレッドを一時停止し、スレッド割り込み例外をスローします |
Condition#awaitUninterruptibly() |
/ | スレッド割り込み例外をスローせずにスレッドを一時停止します。 |
Condition#await(time, unit) |
Object#wait(timeoutMillis, nanos) |
スレッドがウェイクアップされるまでスレッドを一時停止するか、指定された時間待機します。タイムアウト後に自動的にウェイクアップして false を返し、それ以外の場合は true を返します。 |
Condition#awaitUntil(deadline) |
/ | スレッドがウェイクアップされるか、指定された時点に到達するまでスレッドを一時停止します。タイムアウト後は自動的にウェイクアップして false を返します。それ以外の場合は true を返します。 |
Condition#awaitNanos(nanosTimeout) |
/ | スレッドがウェイクアップされるまでスレッドを一時停止するか、指定された時間待機します。戻り値はウェイクアップ時の残り時間を示します (nanosTimeout-時間がかかる)。結果はタイムアウトを示す負の値になります。 |
上記の違いに加えて、Condition
複数の待機キューの作成もサポートしています。つまり、同じロックに複数の待機キューがあり、スレッドは異なるキューで待機し、Object
待機キューは 1 つだけです。「The Art of Java Concurrent Programming」にも同様の表があり、参考までにここに示します。
ヒント:
- 実際、信号をウェイクアップと訳すのは適切ではありません~~
- 関連する実装部分については
Condition
、以下の AQS で詳しく説明しますConditionObject
。
コンディションの使い方は?
Condition
提供されているwaiting関数やwake関数と同じなのでObject
、使い方は似ているのではないでしょうか?
and の呼び出しObject#wait
と同様に、変更されたコード (Monitor の取得)Object#notifyAll
内に存在する必要があります。and の呼び出しの前提は、最初にロックを取得することです。ただし、違いは、使用する前に、ロックを通じて作成する必要があることです。synchronized
Condition#await
Condition#signalAll
Condition
Condition
に示さReentrantLock
れている例ではCondition
、最初にCondition
オブジェクトを作成します。
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
次に、ロックを取得してawait
メソッドを呼び出します。
new Thread(() -> {
lock.lock();
try {
condition.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
lock.unlock();
}
最後に、singalAll
次を呼び出して、ブロックされているすべてのスレッドを起動します。
new Thread(() -> {
lock.lock();
condition.signalAll();
lock.unlock();
}
ConditionObjectのソースコード分析
AQS の内部クラスのみがJava のインターフェイスを実装しているため、インターフェイスとしてはCondition
非常に悲惨です。ConditionObject
Condition
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
public class ConditionObject implements Condition, java.io.Serializable {
private transient Node firstWaiter;
private transient Node lastWaiter;
}
static final class Node {
// 省略
}
}
ConditionObject
Node
フィールドはチェーン構造の先頭ノードと末尾ノードの2 種類のみで、ConditionObject
それらを介して実装される待機キューです。では、ConditionObject
待機列はどのような役割を果たしているのでしょうか? AQS のキュー メカニズムに似ていますか? これら 2 つの質問を念頭に置いて、ソース コードの分析を開始します。
awaitメソッドの実装
Condition
このインターフェイスでは、スレッド待機の 4 つのメソッドが定義されています。
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;
多くの方法がありますが、それらの違いは小さく、時間の処理にのみ反映されます。最も一般的に使用される方法を見てみましょう。
public final void await() throws InterruptedException {
// 线程中断,抛出异常
if (Thread.interrupted()) {
throw new InterruptedException();
}
// 注释1:加入到Condition的等待队列中
Node node = addConditionWaiter();
// 注释2:释放持有锁(调用AQS的release)
int savedState = fullyRelease(node);
int interruptMode = 0;
// 注释3:判断是否在AQS的等待队列中
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
// 中断时退出方法
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) {
break;
}
}
// 加入到AQS的等待队列中,调用AQS的acquireQueued方法
if (acquireQueued(node, savedState) && interruptMode != THROW_IE) {
interruptMode = REINTERRUPT;
}
// 断开与Condition队列的联系
if (node.nextWaiter != null) {
unlinkCancelledWaiters();
}
if (interruptMode != 0) {
reportInterruptAfterWait(interruptMode);
}
}
コメント 1 の部分では、呼び出しメソッドがキューaddConditionWaiter
に追加されます。Condition
private Node addConditionWaiter() {
// 判断当前线程是否为持有锁的线程
if (!isHeldExclusively()) {
throw new IllegalMonitorStateException();
}
// 获取Condition队列的尾节点
Node t = lastWaiter;
// 断开不再位于Condition队列的节点
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
// 创建Node.CONDITION模式的Node节点
Node node = new Node(Node.CONDITION);
if (t == null) {
// 队列为空的场景,将node设置为头节点
firstWaiter = node;
} else {
// 队列不为空的场景,将node添加到尾节点的后继节点上
t.nextWaiter = node;
}
// 更新尾节点
lastWaiter = node;
return node;
}
Condition
のキューは単純な二重リンク リストであり、メソッドが呼び出されるたびにキューの末尾addConditionWaiter
に追加されることがわかります。Condition
注 2 の部分では、スレッドによって保持されているロックが解放され、同時に AQS キューが削除され、AQSrelease
メソッドが内部的に呼び出されます。
=final int fullyRelease(Node node) {
try {
int savedState = getState();
if (release(savedState)) {
return savedState;
}
throw new IllegalMonitorStateException();
} catch (Throwable t) {
node.waitStatus = Node.CANCELLED;
throw t;
}
}
release
AQS の手法とReentrantLock
実装方法についてはすでに分析されているためtryRelease
、ここでは繰り返しません。
注3の部分では、isOnSyncQueue
現在のスレッドがAQSの待機キューに入っているかどうかを判定していますが、このときの様子を見てみましょう。
isOnSyncQueue
が返された場合false
、つまりスレッドが AQS キューにない場合は、スピンに入り、LockSupport#park
スレッドを一時停止するように呼び出します。isOnSyncQueue
を返した場合true
、つまり、スレッドは AQS キュー内にあり、スピンには入らず、後続のロジックを実行します。
注 1 と注 2 の部分を組み合わせると、Condition#await
実装原則は非常に明確になります。
Condition
AQS とは別に待機キューを維持し、相互に排他的です。つまり、同じノードは 1 つのキューにのみ表示されます。- 呼び出されると
Condition#await
、スレッドをCondition
キューに追加し (注 1)、AQS キューから削除します (注 2)。 - 次に、スレッドが配置されているキューを特定します。
- が
Condition
キュー内にあるため、スレッドを一時停止する必要があります。 を呼び出しますLockSupport#park
。 - AQS キュー内にあるスレッドは、ロックの取得を待機しています。
- が
Condition#signalAll
上記の結論に基づいて、ウェイクアップ メソッドの原理はすでに推測できます。
- スレッドを
Condition
キューから削除し、AQS キューに追加します。 - 呼び出して
LockSupport.unpark
スレッドを起動します。
この推測が正しいかどうかについては、ウェイクアップ メソッドの実装を見てみましょう。
ヒント: AQS の関連メソッドがどのように実装されているかを忘れた場合は、「AQS の現在の生活、JUC の基礎の構築」を見直すことができます。
signal メソッドと signalAll メソッドの実装
合計のソースコードsignal
を見てください。signalAll
// 唤醒一个处于等待中的线程
public final void signal() {
if (!isHeldExclusively()) {
throw new IllegalMonitorStateException();
}
// 获取Condition队列中的第一个节点
Node first = firstWaiter;
if (first != null) {
// 唤醒第一个节点
doSignal(first);
}
}
// 唤醒全部处于等待中的线程
public final void signalAll() {
if (!isHeldExclusively()){
throw new IllegalMonitorStateException();
}
Node first = firstWaiter;
if (first != null) {
// 唤醒所有节点
doSignalAll(first);
}
}
2 つのメソッドの唯一の違いは、ヘッド ノードが空でないときにdoSignal
1 つのスレッドをウェイクアップするために呼び出すか、すべてのスレッドをウェイクアップするために呼び出すかどうかですdoSignalAll
。
private void doSignal(Node first) {
do {
// 更新头节点
if ( (firstWaiter = first.nextWaiter) == null) {
// 无后继节点的场景
lastWaiter = null;
}
// 断开节点的连接
first.nextWaiter = null;
// 唤醒头节点
} while (!transferForSignal(first) && (first = firstWaiter) != null);
}
private void doSignalAll(Node first) {
// 将Condition的队列置为空
lastWaiter = firstWaiter = null;
do {
// 断开链接
Node next = first.nextWaiter;
first.nextWaiter = null;
// 唤醒当前头节点
transferForSignal(first);
// 更新头节点
first = next;
} while (first != null);
}
否が応でもdoSignal
キューからノードをdoSignalAll
削除するだけで、実際にウェイクアップするメソッドであることが分かります メソッド名から、このメソッドが「 transfer 」によってウェイクアップすることがわかります。ソースコードを見てみましょう:Condition
transferForSignal
final boolean transferForSignal(Node node) {
// 通过CAS替换node的状态
// 如果替换失败,说明node不处于Node.CONDITION状态,不需要唤醒
if (!node.compareAndSetWaitStatus(Node.CONDITION, 0)) {
return false;
}
// 将节点添加到AQS的队列的队尾
// 并返回老队尾节点,即node的前驱节点
Node p = enq(node);
int ws = p.waitStatus;
// 对前驱节点状态的判断
if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL)) {
LockSupport.unpark(node.thread);
}
return true;
}
transferForSignal
このメソッドでは、呼び出し元のenq
メソッドがnode
AQS キューに再度追加され、node
プリカーサー ノードが返されて、プリカーサー ノードの状態が判断されます。
- 現在ws > 0 ws > 0うーん_>0では、先行ノードは
Node.CANCELLED
その状態にあり、先行ノードはロック競合から抜け出し、node
直接ウェイクアップできます。 - 現在ws ≤ 0 ws \leq 0うーん_≤0の場合、CAS を通じて先行ノードのステータスを変更し
Node.SIGNAL
、設定が失敗した場合に直接ウェイクアップしますnode
。
「AQS の生活、JUC の基盤の構築」で紹介されたwaitStatus
5 つの状態。Node.SIGNAL
この状態は、後継ノードを起動する必要があることを示します。さらに、shouldParkAfterFailedAcquire
メソッドのソース コードを分析すると、AQS 待機キューに入るときに、プリカーサー ノードの状態を に更新する必要があることがわかりますNode.SIGNAL
。
最後にenq
実装を見てみましょう。
private Node enq(Node node) {
for (;;) {
// 获取尾节点
Node oldTail = tail;
if (oldTail != null) {
// 更新当前节点的前驱节点
node.setPrevRelaxed(oldTail);
// 更新尾节点
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
// 返回当前节点的前驱节点(即老尾节点)
return oldTail;
}
} else {
initializeSyncQueue();
}
}
}
enq
実装は非常に簡単で、CAS を介して AQS キューの末尾ノードを更新することは、それを AQS のキューに追加し、末尾ノードの先行ノードを返すことと同じです。さて、ウェイクアップメソッドのソースコードはここにありますが、当初の推測とまったく同じですか?
グラフィカルなConditionObjectの原理
機能的にはAQS版の と がCondition
実装されており、使い方もそれに似ていますが、最初にロックを取得する、つまりと の間で呼び出す必要があります。原則として、これはAQS キューとキューの間でのスレッドの転送にすぎません。Object#wait
Object#notify
lock
unlock
Condition
スレッド t がロックを保持します
スレッド t が取得されReentrantLock
、スレッド t1、t2、および t3 が AQS キューで待機していると仮定すると、次のような構造を取得できます。
スレッド t が Condition#await を実行する
メソッドがスレッド t で呼び出されCondition#await
、スレッド t がCondition
待機キューに入り、スレッド t1 がそれを取得してReentrantLock
AQS キューから削除する場合、構造は次のようになります。
スレッド t1 が Condition#await を実行します
メソッドがスレッド t1 でも実行される場合、スレッド t1 もキューにCondition#await
入り、スレッド t2 がそれを取得します。構造は次のとおりです。Condition
ReentrantLock
スレッド t2 が Condition#signal を実行します
スレッド t2 が実行されると、キュー内の最初のスレッドがCondition#signal
起動され、その構造は次のようになります。Condition
Condition
上記のプロセスを通じて、キューと AQS キューの間でスレッドがどのように転送されるかを取得できます。
エピローグ
内容はCondition
ここまでで、原理を理解したり、使ったり、分析したりすることCondition
は難しくありませんが、使用頻度が低いため、やや馴染みがありません。
最後に、記事のリリース時点で、最初の 2 つの質問に対する答えを書き終えているはずです~~
では、今日はここまでです、さようなら~~