AQS のコンディションとは何ですか?

フォロー歓迎: Wang Youzhi
、バケツを運ぶ Java 人々のグループに参加することを楽しみにしています:一緒に裕福な Java 人々

今すぐチャットに来てくださいCondition。AQS Condition"ファミリー" に待機およびウェイクアップ機能を提供し、AQS "ファミリー" がsynchronizedスレッドと同様にスレッドを一時停止およびウェイクアップできるようにします。まず、Condition面接での次の 2 つの質問を見てみましょう。

  • Conditionwait と wake forObjectはどう違いますか?
  • Conditionキューとは何ですか?

次に、「何を」「どう使うか」「どう実装するか」の順にベールを脱いでいきましょうCondition

条件とは何ですか?

ConditionObject#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 の呼び出しの前提は最初にロックを取得することです。ただし、違いは、使用する前に、ロックを通じて作成する必要があることですsynchronizedCondition#awaitCondition#signalAllConditionCondition

に示さ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非常に悲惨です。ConditionObjectCondition

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 {
    // 省略
  }
}

ConditionObjectNodeフィールドはチェーン構造の先頭ノードと末尾ノードの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;
  }
}

releaseAQS の手法とReentrantLock実装方法についてはすでに分析されているためtryRelease、ここでは繰り返しません。

注3の部分では、isOnSyncQueue現在のスレッドがAQSの待機キューに入っているかどうかを判定していますが、このときの様子を見てみましょう。

  • isOnSyncQueueが返された場合false、つまりスレッドが AQS キューにない場合は、スピンに入り、LockSupport#parkスレッドを一時停止するように呼び出します。
  • isOnSyncQueueを返した場合true、つまり、スレッドは AQS キュー内にあり、スピンには入らず、後続のロジックを実行します。

注 1 と注 2 の部分を組み合わせると、Condition#await実装原則は非常に明確になります。

  • ConditionAQS とは別に待機キューを維持し、相互に排他的です。つまり、同じノードは 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 つのメソッドの唯一の違いは、ヘッド ノードが空でないときにdoSignal1 つのスレッドをウェイクアップするために呼び出すか、すべてのスレッドをウェイクアップするために呼び出すかどうかです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 」によってウェイクアップすることがわかります。ソースコードを見てみましょう:ConditiontransferForSignal

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メソッドがnodeAQS キューに再度追加され、nodeプリカーサー ノードが返されて、プリカーサー ノードの状態が判断されます。

  • 現在ws > 0 ws > 0うーん_>0では、先行ノードはNode.CANCELLEDその状態にあり、先行ノードはロック競合から抜け出し、node直接ウェイクアップできます。
  • 現在ws ≤ 0 ws \leq 0うーん_0の場合、CAS を通じて先行ノードのステータスを変更しNode.SIGNAL、設定が失敗した場合に直接ウェイクアップしますnode

AQS の生活、JUC の基盤の構築」で紹介されたwaitStatus5 つの状態。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#waitObject#notifylockunlockCondition

スレッド t がロックを保持します

スレッド t が取得されReentrantLock、スレッド t1、t2、および t3 が AQS キューで待機していると仮定すると、次のような構造を取得できます。
ここに画像の説明を挿入

スレッド t が Condition#await を実行する

メソッドがスレッド t で呼び出されCondition#await、スレッド t がCondition待機キューに入り、スレッド t1 がそれ​​を取得してReentrantLockAQS キューから削除する場合、構造は次のようになります。
ここに画像の説明を挿入

スレッド t1 が Condition#await を実行します

メソッドがスレッド t1 でも実行される場合、スレッド t1 もキューにCondition#await入り、スレッド t2 がそれを取得します。構造は次のとおりです。ConditionReentrantLock
ここに画像の説明を挿入

スレッド t2 が Condition#signal を実行します

スレッド t2 が実行されると、キュー内の最初のスレッドがCondition#signal起動され、その構造は次のようになります。Condition
ここに画像の説明を挿入

Condition上記のプロセスを通じて、キューと AQS キューの間でスレッドがどのように転送されるかを取得できます。
ここに画像の説明を挿入

エピローグ

内容はConditionここまでで、原理を理解したり、使ったり、分析したりすることConditionは難しくありませんが、使用頻度が低いため、やや馴染みがありません。

最後に、記事のリリース時点で、最初の 2 つの質問に対する答えを書き終えているはずです~~


では、今日はここまでです、さようなら~~

おすすめ

転載: blog.csdn.net/wyz_1945/article/details/130716274