Java同時実行パッケージを簡単な方法で説明する—CountDownLauch原理の分析
Tianyu Star IT Haha
CountDownLauchは、Java同時実行パッケージに設定された同期ツールです。同時実行のカウンターと呼ばれることが多く、ロックもあります。
CountDownLauchは、主に2つのシナリオで使用されます。1つはスイッチと呼ばれ、1つまたはグループのスレッドがタスクが完了するまで継続的に待機できるようにします。この状況はしばしばロックと呼ばれます。素人の用語では、ゲートと同等です。ゲートが開く前にすべてのスレッドがブロックされます。ゲートが開かれると、すべてのスレッドが通過しますが、ゲートが開かれると、すべてのスレッドがブロックされます。合格すると、ロック状態は無効になり、ドアの状態は変更できず、開いた状態のみになります。別のシナリオは、しばしばカウンターと呼ばれます。これにより、タスクをN個の小さなタスクに分割できます。メインスレッドは、すべてのタスクが完了するまで待機します。各タスクが完了すると、すべてのタスクが完了するまで、カウンターが1つ減ります。メインスレッドのブロッキング。
CountDownLauchに対応するAPIを見てみましょう。
CountDownLatchは正のカウンターを維持し、countDownメソッドはカウンターをデクリメントし、awaitメソッドはカウンターが0に達するのを待ちます。すべての待機スレッドは、カウンターが0に達するか、待機スレッドが中断またはタイムアウトするまでブロックされます。
対応するアプリケーションの例を見てみましょう。
package com.yhj.lauth;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
//工人
class Worker extends Thread{
privateintworkNo;//工号
private CountDownLatch startLauch;//启动器-闭锁
private CountDownLatch workLauch;//工作进程-计数器
public Worker(int workNo,CountDownLatch startLauch,CountDownLatch workLauch) {
this.workNo = workNo;
this.startLauch = startLauch;
this.workLauch = workLauch;
}
@Override
publicvoid run() {
try {
System.out.println(new Date()+" - YHJ"+workNo+" 准备就绪!准备开工!");
startLauch.await();//等待老板发指令
System.out.println(new Date()+" - YHJ"+workNo+" 正在干活...");
Thread.sleep(100);//每人花100ms干活
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
System.out.println(new Date()+" - YHJ"+workNo+" 工作完成!");
workLauch.countDown();
}
}
}
//测试用例
publicclass CountDownLauthTestCase {
publicstaticvoid main(String[] args) throws InterruptedException {
int workerCount = 10;//工人数目
CountDownLatch startLauch = new CountDownLatch(1);//闭锁相当于开关
CountDownLatch workLauch = new CountDownLatch(workerCount);//计数器
System.out.println(new Date()+" - Boss:集合准备开工了!");
for(int i=0;i<workerCount;++i){
new Worker(i, startLauch, workLauch).start();
}
System.out.println(new Date()+" - Boss:休息2s后开工!");
Thread.sleep(2000);
System.out.println(new Date()+" - Boss:开工!");
startLauch.countDown();//打开开关
workLauch.await();//任务完成后通知Boss
System.out.println(new Date()+" - Boss:不错!任务都完成了!收工回家!");
}
}
执行结果:
Sat Jun 08 18:59:33 CST 2013 - Boss:集合准备开工了!
Sat Jun 08 18:59:33 CST 2013 - YHJ0 准备就绪!准备开工!
Sat Jun 08 18:59:33 CST 2013 - YHJ2 准备就绪!准备开工!
Sat Jun 08 18:59:33 CST 2013 - YHJ1 准备就绪!准备开工!
Sat Jun 08 18:59:33 CST 2013 - YHJ4 准备就绪!准备开工!
Sat Jun 08 18:59:33 CST 2013 - Boss:休息2s后开工!
Sat Jun 08 18:59:33 CST 2013 - YHJ8 准备就绪!准备开工!
Sat Jun 08 18:59:33 CST 2013 - YHJ6 准备就绪!准备开工!
Sat Jun 08 18:59:33 CST 2013 - YHJ3 准备就绪!准备开工!
Sat Jun 08 18:59:33 CST 2013 - YHJ7 准备就绪!准备开工!
Sat Jun 08 18:59:33 CST 2013 - YHJ5 准备就绪!准备开工!
Sat Jun 08 18:59:33 CST 2013 - YHJ9 准备就绪!准备开工!
Sat Jun 08 18:59:35 CST 2013 - Boss:开工!
Sat Jun 08 18:59:35 CST 2013 - YHJ0 正在干活...
Sat Jun 08 18:59:35 CST 2013 - YHJ2 正在干活...
Sat Jun 08 18:59:35 CST 2013 - YHJ1 正在干活...
Sat Jun 08 18:59:35 CST 2013 - YHJ4 正在干活...
Sat Jun 08 18:59:35 CST 2013 - YHJ8 正在干活...
Sat Jun 08 18:59:35 CST 2013 - YHJ6 正在干活...
Sat Jun 08 18:59:35 CST 2013 - YHJ3 正在干活...
Sat Jun 08 18:59:35 CST 2013 - YHJ7 正在干活...
Sat Jun 08 18:59:35 CST 2013 - YHJ5 正在干活...
Sat Jun 08 18:59:35 CST 2013 - YHJ9 正在干活...
Sat Jun 08 18:59:35 CST 2013 - YHJ5 工作完成!
Sat Jun 08 18:59:35 CST 2013 - YHJ1 工作完成!
Sat Jun 08 18:59:35 CST 2013 - YHJ3 工作完成!
Sat Jun 08 18:59:35 CST 2013 - YHJ6 工作完成!
Sat Jun 08 18:59:35 CST 2013 - YHJ7 工作完成!
Sat Jun 08 18:59:35 CST 2013 - YHJ9 工作完成!
Sat Jun 08 18:59:35 CST 2013 - YHJ4 工作完成!
Sat Jun 08 18:59:35 CST 2013 - YHJ0 工作完成!
Sat Jun 08 18:59:35 CST 2013 - YHJ2 工作完成!
Sat Jun 08 18:59:35 CST 2013 - YHJ8 工作完成!
Sat Jun 08 18:59:35 CST 2013 - Boss:不错!任务都完成了!收工回家!
この例では、2つのCountDownLauchsを使用してそれぞれ2つのシナリオを作成します。最初のstartLauchはスイッチに相当します。オンになる前はスレッドは実行されません。オンにすると、すべてのスレッドを同時に実行できます。2番目のworkerLauchは実際にはカウンターです。カウンターがゼロに減らされていない場合、メインスレッドは永久に待機します。すべてのスレッドが実行されると、メインスレッドはブロックを解除して実行を続行します。
2番目のシナリオは、後で学習するスレッドプールでよく使用されます。これについては、後で説明します。
ここには、
メモリの一貫性の効果である重要な機能もあります。スレッドでcountDown()を呼び出す前に発生する操作は、別のスレッドからのawait()の正常な戻りに対応する操作の直後に続きます。
シーンアプリケーションを見てきましたが、それはどのような原則に基づいており、どのように実現されていますか?
対応するソースコードを見てみましょう。
privatestaticfinalclass Sync extends AbstractQueuedSynchronizer
クラスの2行目には、内部にAQSを実装するシンクロナイザーがあります。使用するメソッド、awaitとcountDownに焦点を当てましょう。最初にawaitメソッドを見てください
publicvoid await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
明らかに、内部の再実装されたシンクロナイザーを直接呼び出して、共有ロック方式を取得することです(以前は排他ロックについて話していましたが、今日はこの機会に共有ロックメカニズムについて一緒に話します)。
publicfinalvoid acquireSharedInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
thrownew InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
ここで、スレッドが中断された場合、スレッドは直接終了します。それ以外の場合は、共有ロックを取得しようとします。tryAcquireShared(arg)の実装を見てみましょう(このメソッドは内部クラスによってオーバーライドされます)。
publicint tryAcquireShared(int acquires) {
return getState() == 0? 1 : -1;
}
いわゆる共有ロックとは、ロックを共有するすべてのスレッドが同じリソースを共有することを意味します。いずれかのスレッドが共有リソースを取得すると、すべてのスレッドが同じリソースを持ちます。つまり、通常の状況では、共有ロックは単なる記号であり、すべてのスレッドはこの記号が満たされるかどうかを待機します。満たされると、すべてのスレッドがアクティブ化されます(すべてのスレッドがロックを取得するのと同じです)。ここでのロックCountDownLatchは、共有ロックの実現に基づいています。そして明らかに、ここでの識別子は、状態などがゼロに等しくないことであり、状態は実際にはこのリソースをめぐって競合しているスレッドの数です。これは0より大きいデータであり、コンストラクターを介して渡されるため、この時点でここに戻ります。常に-1です。
Sync(int count) {
setState(count);
}
tryAcquireSharedによって返されるデータがゼロ未満の場合は、リソースが取得されておらず、ブロックする必要があることを意味します。このとき、コードdoAcquireSharedInterruptibly()が実行されます。
privatevoid doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
break;
}
} catch (RuntimeException ex) {
cancelAcquire(node);
throw ex;
}
// Arrive here only if interrupted
cancelAcquire(node);
thrownew InterruptedException();
}
ここでは、最初に共有モードでCLHキューにノードを追加し、次に現在のノードの先行ノードを確認します(挿入されたデータはキューの最後にあります)。先行ノードがヘッドノードであり、現在のカウンターが0の場合、ウェイクアップします。後続ノード(後でウェイクアップ)、それ以外の場合は、必要に応じて現在のスレッドをブロックするかどうかを決定します!目覚めるか中断されるまで!
privatefinalboolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
ここで、LockSupport.park(Obj)のパラメーターobjは、ブロックされたオブジェクトではなく、ブロックされた監視オブジェクトであることに注意してください。ブロックされたオブジェクトは現在の操作のスレッドであるため、解凍時に対応するスレッドを解決する必要があります。混同しないでください!
publicstaticvoid park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
unsafe.park(false, 0L);
setBlocker(t, null);
}
publicstaticvoid unpark(Thread thread) {
if (thread != null)
unsafe.unpark(thread);
}
対応するcountDownメソッドの実装を見てみましょう
publicvoid countDown() {
sync.releaseShared(1);
}
まず、countDownが実行されるたびに、内部メソッドのロック解除操作が実行されます!
publicfinalboolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
returntrue;
}
returnfalse;
}
試行が成功した場合は、現在のノードをヘッドノードとして設定し、対応するノードの後続のノードをウェイクアップします。
publicboolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
returnfalse;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
同様に、ロックを解放するメソッドも、CountDownLauch内の同期クラスによって実装されます。このメソッドは、現在のカウンターの数を検出するために回転します。ゼロに等しい場合は、以前にブロックされたすべてのスレッドが解放されたことを意味します。それ以外の場合は、CASが現在のカウンターを設定し、デクリメントします。カウントダウン番号に移動します。設定が成功した後にデータがゼロの場合は、実行が完了し、ブロックされたスレッドを解放する必要があることを意味します。trueを返します(ここでは微妙なreturn nextc == 0であることに注意してください)。それ以外の場合はfalseを返します。
releaseSharedメソッドをもう一度振り返ってみましょう。tryReleaseSharedがtrueを返す場合、カウンターがゼロに達し、ブロックされたリソースを解放する必要があることを意味します。このとき、unparkSuccessor(h)メソッドを実行して、キュー内のヘッドノードをウェイクアップします。
ここで、デリケートキューは、singleAllと同様の方法ですべてのスレッドを直接ウェイクアップするのではなく、ブロックされたスレッドを順番に解放するように設計されています。それで、それはどのように機能しますか?このコードでは、ヘッドノードのみが起動されます(実際、これはヘッドノードの後続ノードであり、ヘッドノードは単なる空のノードです)。最初に、unparkSuccessorの実装を見てみましょう。
privatevoid unparkSuccessor(Node node) {
/*
* Try to clear status in anticipation of signalling. It is
* OK if this fails or if status is changed by waiting thread.
*/
compareAndSetWaitStatus(node, Node.SIGNAL, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
明らかに、着信パラメータがヘッドノードであることがわかります。CASを介してデータを設定した後、ヘッドノードの後続のノードが起動されます(アンパックはスレッドであり、ブロッキングモニターではないことに注意してください)。それから私は戻った!
残りのブロックされたスレッドはどのようにウェイクアップしますか?awaitメソッドでのdoAcquireSharedInterruptiblyの実装を見てみましょう
privatevoid doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg); // tag 2
if (r >= 0) {
setHeadAndPropagate(node, r); // tag 3
p.next = null; // help GC
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())// tag 1
break;
}
} catch (RuntimeException ex) {
cancelAcquire(node);
throw ex;
}
// Arrive here only if interrupted
cancelAcquire(node);
thrownew InterruptedException();
}
以前、parkAndCheckInterrupt()が実行されたときにブロックが実行されたことがわかります。ヘッドノードの後続ノード(キューに入る最初のノード)をウェイクアップすると、tag1のこのコード行がウェイクアップされ、ブレーク後もスピンを続け、このとき、tag2のコード行はカウンターがすでに0であることを検出するため、tryAcquireShared(arg)によって返される結果は1(前に返されるものはすべて-1)、rはゼロより大きく、tag3コードを入力すると、tag3は現在のスレッドをヘッドエンドとして設定します。ポイントしてから、後続の後続ノードをウェイクアップし続けます。
privatevoid setHeadAndPropagate(Node node, int propagate) {
setHead(node); // tag 4
if (propagate > 0 && node.waitStatus != 0) {
/*
* Don't bother fully figuring out successor. If it
* looks null, call unparkSuccessor anyway to be safe.
*/
Node s = node.next;
if (s == null || s.isShared())
unparkSuccessor(node); // tag 5
}
}
後続ノードがウェイクアップされた後、後続ノードは引き続きウェイクアップし、次にキュー内のデータを順番にウェイクアップします。
CountDownLatch全体は次のようになります。実際、アトミック操作とAQSの原則と実装により、CountDownLatchの分析は比較的簡単です。