今日は、AQS ファミリーの「外部弟子」である CyclicBarrier について学びます。
CyclicBarrier が AQS ファミリーの「外部弟子」であると言われるのはなぜですか? これは、CyclicBarrier 自体と内部クラス Generation が AQS を継承せず、ソース コードの実装において AQS ファミリのメンバーである ReentrantLock に大きく依存しているためです。秀仙の小説のように、大家族は外宗と内宗を区別し、外宗の弟子は内宗の弟子の評判を利用して行動することが多いのですが、『CyclicBarrier』もまさにこれと同じで、 AQSファミリーの「外宗弟子」。実際の面接では、 CyclicBarrier は CountDownLatch に比べて出現頻度は低く、通常は質問中に出現します。
今日は、CyclicBarrier を段階的に逆アセンブルして、CountDownLatch との違いを確認します。
サイクリックバリアとは何ですか?
CyclicBarrier の名前から始めましょう。Cyclic は形容詞で、「円形、周期的」と訳され、Barrier は名詞で、「障壁、フェンス」と訳されます。この組み合わせは「円形の障壁」です。では、「」をどのように理解すればよいでしょうか。周期的な「?バリア」? CyclicBarrier のアノテーションがどのように説明されるかを見てみましょう。
一連のスレッドが互いに共通のバリア ポイントに到達するのを待機できるようにする同期補助。CyclicBarrier は、一連のスレッドが共通のバリア ポイントに到達するのを互いに待機できるようにする同期補助です。
バリアは、待機中のスレッドが解放された後に再利用できるため、循環と呼ばれます。
これは CountDownLatch にいくらか似ています。CyclicBarrier がどのように機能するかを図で見てみましょう。
一部のスレッドがバリアに到達すると、スレッドはバリアで待機し、すべてのスレッドがバリアに到達した後でのみ実行を継続します。CountDownLatchでのクロスカントリー ハイキングを例に挙げると、ボスを排除し、プレイヤー間でお互いを待ちます。これが CyclicBarrier です。
さらに、CyclicBarrier は「再利用」、つまり再利用できるとコメントにあります。CountDownLatch の実装はカウンターをリセットするための作業を行わないことを思い出してください。つまり、CountDownLatch のカウントが 0 に減少すると、それを復元することはできません。これは、CountDownLatch の機能が 1 回限りであることを意味します。
ヒント: 実際、CountDownLatch を使用して CyclicBarrier と同様の関数を実装できます。
サイクリックバリアの使い方は?
例として、ボスのいないクロスカントリー ハイキングを考えてみましょう。最初に到着したプレイヤーの一部は、後で到着したプレイヤーが一緒に昼食をとるのを待つ必要があります。CyclicBarrier を使用してこれを実装するコードは次のとおりです。
// 初始化CyclicBarrier
CyclicBarrier cyclicBarrier = new CyclicBarrier(10);
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep((finalI + 1));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
System.out.println("选手[" + finalI + "]到达终点,等待其他选手!!!");
// 线程在屏障点处等待
cyclicBarrier.await();
System.out.println("选手[" + finalI + "]开始吃午饭啦!!!");
} catch (InterruptedException | BrokenBarrierException e) {
throw new RuntimeException(e);
}
}).start();
}
使用法は CountDownLatch と非常に似ています。コンストラクターは、一斉に動作するために CyclicBarrier がバリアに到達する必要があるスレッドの数を設定します。違いは、CyclicBarrier が各スレッドで呼び出されCyclicBarrier#await
、CountDownLatch を使用するときにメイン スレッドで 1 回だけ呼び出すことですCountDownLatch#await
。
CountDownLatch をスレッド内で呼び出すことはできますかCountDownLatch#await
? 答えは「はい」です。これを使用した場合の効果は CyclicBarrier と同じです。
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep((finalI + 1));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("选手[" + finalI + "]到达终点!!!");
countDownLatch.countDown();
try {
countDownLatch.await();
System.out.println("选手[" + finalI + "]开始吃午饭啦!!!");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
}
CyclicBarrier#await
上記の例から、メソッドがCountDownLatch#countDown
メソッドとメソッドCountDownLatch#await
の能力を同時に備えている、つまり、実行回数が 1 つ減り、中断されたスレッドが実行されると考えるのは難しくありません。
CyclicBarrier はどのように実装されますか?
CyclicBarrier 全体を見てみましょう。
CyclicBarrier の内部構造は CountDownLatch よりも複雑です。AQS の助けを借りて前述した「内部弟子」の ReentrantLock タイプのロックと Condition タイプのトリップに加えて、CyclicBarrier には 2 つの「特別な」場所があります。
- 内部クラス Generation は、直訳すると「世代」ですが、どのような役割を果たしますか?
- Runnable型のメンバ変数barrierCommandは何をするのでしょうか?
残りについては、対応するメソッドのほとんどは CountDownLatch で見つけることができ、または名前からその機能を簡単に知ることができます。
CyclicBarrierの構築方法
CyclicBarrier は 2 つ (実際には 1 つ) のコンストラクターを提供します。
// 需要到达屏障的线程数
private final int parties;
// 所有线程都到达后执行的动作
private final Runnable barrierCommand;
// 计数器
private int count;
public CyclicBarrier(int parties) {
this(parties, null);
}
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) {
throw new IllegalArgumentException();
}
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
2 番目のコンストラクターは 2 つのパラメーターを受け取ります。
- party: を呼び出すためにバリアに到達する必要があるスレッドの数を示します
CyclicBarrier#await
。 - BarrierAction: すべてのスレッドがバリアに到達した後に実行されるアクション。
構築方法のコードは相変わらずシンプルで、疑問が生じやすい点が 1 つだけあります。party と count の違いは何ですか?
まずメンバー変数の宣言を確認します。パーティは、final を使用し、それが不変オブジェクトであることを示します。これは、CyclicBarrier が一緒にバリアに到達するために複数のスレッドが必要であることを意味します。count はカウンター、初期値はパーティの数です。バリアに到達したスレッド数が増加すると、徐々に 0 まで減少します。
CyclicBarrier の内部クラスの生成
private static class Generation {
Generation() {}
boolean broken;
}
Generation は、CyclicBarrier の現在の世代をマークするために使用されます。Doug Lea はその役割を次のように説明しています。
バリアの各使用は、生成インスタンスとして表されます。バリアが解除されるか、リセットされるたびに世代が変わります。
バリア (CyclicBarrier) を使用するたびに、Generation インスタンスが必要です。バリアを通過するかリセットするかで世代が変わります。
Generation の Broken は、現在の CyclicBarrier が壊れているかどうかをマークするために使用されます。デフォルトは false です。値が true の場合、現在の CyclicBarrier が壊れていることを意味します。現時点では、CyclicBarrier は正常に使用できません。 CyclicBarrier の状態をリセットするには、 を呼び出す必要がありますCyclicBarrier#reset
。
CyclicBarrier#await方法
CyclicBarrier#await
このメソッドは、カウント - 1 を実現するだけでなく、スレッド待機関数も実現すると推測しました。次に、ソース コードを通じてアイデアを検証します。
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe);
}
}
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
どちらのオーバーロードもメソッドを指しますCyclicBarrier#dowait
。
private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException {
// 使用ReentrantLock
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 第2部分
// 获取CyclicBarrier的当前代,并检查CyclicBarrier是否被打破
final Generation g = generation;
if (g.broken) {
throw new BrokenBarrierException();
}
// 线程被中断时,调用breakBarrier方法
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
// 第3部分
//计数器减1
int index = --count;
// 计数器为0时表示所有线程都到达了,此时要做的就是唤醒等待中的线程
if (index == 0) {
boolean ranAction = false;
try {
// 执行唤醒前的操作
final Runnable command = barrierCommand;
if (command != null) {
command.run();
}
ranAction = true;
// CyclicBarrier进入下一代
nextGeneration();
return 0;
} finally {
if (!ranAction) {
breakBarrier();
}
}
}
// 第4部分
// 只有部分线程到达屏障处的情况
for (;;) {
try {
//调用等待逻辑)
if (!timed) {
trip.await();
} else if (nanos > 0L) {
nanos = trip.awaitNanos(nanos);
}
} catch (InterruptedException ie) {
// 线程被中断时,调用breakBarrier方法
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
Thread.currentThread().interrupt();
}
}
if (g.broken) {
throw new BrokenBarrierException();
}
// 如果不是当前代,返回计数器的值
if (g != generation) {
return index;
}
// 如果等待超时,调用breakBarrier方法
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
CyclicBarrier#dowait
このメソッドは長く見えますが、ロジックは次の 3 つの部分に分かれていれば複雑ではありません。
- パート 1: CyclicBarrier とスレッドのステータスの検証。
- パート 2: カウンタが 1 減分され、値が 0 になったら、待機中のすべてのスレッドを起動します。
- その 3: カウンタが 1 減算されて値が 0 でない場合、スレッドは待ち状態になります。
まず最初の部分、CyclicBarrier とスレッド状態の検証の部分を見てみましょう。まず、CyclicBarrier が壊れているかどうかを判断し、次に現在のスレッドが中断状態にあるかどうかを判断します。そうであれば、次のメソッドを呼び出しますCyclicBarrier#breakBarrier
。
private void breakBarrier() {
generation.broken = true;
count = parties;
trip.signalAll();
}
CyclicBarrier#breakBarrier
この方法は非常にシンプルで、実行することは 3 つだけです。
- CyclicBarrier が壊れていることをマークします。
- CyclicBarrier のカウンターをリセットします。
- 待機中のすべてのスレッドを起動します。
つまり、スレッドが中断としてマークされると、CyclicBarrier バリアを直接突破します。
まずパート 2 のウェイクアップ ロジックをスキップして、パート 3 の待機状態に入るスレッドのロジックに直接進みましょう。timed パラメーターに従って、Condition のさまざまな待機メソッドを呼び出すことを選択し、続いて例外とスレッド割り込みステータスの処理も呼び出され、CyclicBarrier は使用CyclicBarrier#breakBarrier
不可としてマークされます。スレッドが待機状態に入るロジックは複雑ではなく、基本的には AQS の Condition によって実現されます。
最後に、パート 2 で待機中のすべてのスレッドを起こす動作を見て、カウンタが 0 かどうかで目覚めるかどうかを判断します。ウェイクアップする必要がある場合、最後に実行中のCyclicBarrier#await
スレッドは、barrierCommand を実行し (この時点ではスレッドウェイクアップ操作は実行されていません)、バリアを通過する前に処理を行ってから、メソッドを呼び出しますCyclicBarrier#nextGeneration
。
private void nextGeneration() {
trip.signalAll();
count = parties;
generation = new Generation();
}
CyclicBarrier#nextGeneration
このメソッドは次の 3 つのことも実行します。
- Condition を待機しているすべてのスレッドを起動します。
- CyclicBarrier のカウンターをリセットします。
- 新しい世代オブジェクトを作成します。
「次世代」に入るという名の通り、まず「前の世代」で待機中のスレッドをすべてウェイクアップし、次にCyclicBarrierのカウンタをリセットし、最後にCyclicBarrierのGenerationオブジェクトを更新し、CyclicBarrierをリセットし、 CyclicBarrier を次の時代に迎えましょう。
ここで、CyclicBarrier 自体はカウンターの維持とリセットの作業のみを実行し、相互排除とスレッドの待機とウェイクアップは AQS ファミリのメンバーによって実行されることを見つけるのは難しくありません。
- ReentrantLock は、同時に 1 つのスレッドだけが実行できること
CyclicBarrier#await
、つまり、同時に 1 つのスレッドだけがカウンターを維持できることを保証します。 - Condition は、CyclicBarrier に条件付き待機キューを提供し、スレッドの待機とウェイクアップの作業を完了します。
CyclicBarrier#reset方法
CyclicBarrier#reset
最後に、メソッドを見てみましょう。
public void reset() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 主动打破CyclicBarrier
breakBarrier();
// 使CyclicBarrier进入下一代
nextGeneration();
} finally {
lock.unlock();
}
}
CyclicBarrier#reset
メソッドは古いものばかりで、まずCyclicBarrier#breakBarrier
前世代の CyclicBarrier を壊します。やり直したいので過去を「思い出す」のではなく、最後に呼び出してCyclicBarrier#nextGeneration
新しい時代を始めます。ここでのロックの目的は、実行中にCyclicBarrier#reset
スレッドがCyclicBarrier#await
メソッドを実行しないようにすることであることに注意してください。
さて、ここでは CyclicBarrier の核となる内容を分析しましたが、残りのメソッドは非常に単純なので、誰もがその機能を理解し、名前からその実現を推測できると思います。
ヒント:CyclicBarrier#getNumberWaiting
真ん中に鍵があるのですが、これはなぜですか?
CountDownLatch と Cyclicbarrier の違いは何ですか?
最後のパートでは、記事冒頭のインタビューの質問に答えてみましょう。CountDownLatch と Cyclicbarrier の違いは何ですか?
ポイント 1: CyclicBarrier は再利用できますが、CountDownLatch は再利用できません。
通常の使用が終了した場合でも、CyclicBarrier#reset
メソッドの呼び出しが終了した場合でも、Cyclicbarrier は内部カウンターをリセットできます。
ポイント 2: Cyclicbarrierはメソッドを呼び出すスレッドのみをブロックしますが、CountDownLatch は 1 つ以上のスレッドをブロックできます。CyclicBarrier#await
CountDownLatch は 1 ずつカウントダウンとブロックをCountDownLatch#countDown
2 つのCountDownLatch#await
メソッドに分割しますが、Cyclicbarrier はCyclicBarrier#await
2 つのステップのみを完了します。同一スレッド内で和CountDownLatch#countDown
が連続であればCountDownLatch#await
メソッドとCyclicBarrier#await
同じ機能を実現します。
この記事が役に立った場合は、たくさんの賞賛とサポートをお願いします。記事に間違いがあった場合は、批判と修正をお願いします。最後に、皆さんもぜひ、ハードコア Java テクノロジーを共有する金融マン。