CountDownLatchの深い理解

概念

CountDownLatchを使用して、実行を続行する前に他のスレッドが特定の操作のセットを完了するのを待機している1つ以上のスレッドを実装できます。この一連の操作は、前提条件操作と呼ばれます

使用する

public static void main(String[] args) throws InterruptedException {
    
    
    CountDownLatch countDownLatch=new CountDownLatch(2);

    new Thread(()->{
    
    
        System.out.println(Thread.currentThread().getName()+ "->begin");
        countDownLatch.countDown();//2-1
        System.out.println(Thread.currentThread().getName()+ "->end");
    }).start();

    new Thread(()->{
    
    
        System.out.println(Thread.currentThread().getName()+ "->begin");
        countDownLatch.countDown();//1-1=0
        System.out.println(Thread.currentThread().getName()+ "->end");
    }).start();

    countDownLatch.await();//阻塞,count=0时唤醒
}

動作結果:

ここに写真の説明を挿入
CountDownLatchは、処理の操作のを表すカウンターを内部的に維持しますCountDownLatch.countDown()は、実行されるたびに、対応するインスタンスのカウンター値を1つ減らします。
CountDownLatch.await()はブロッキングメソッドです。カウンタ値が0の場合、すべての前提条件操作が実行され、ブロックされたスレッドが起動されたことを意味します

ソースコード分析

注:ソースコードで各メソッドの機能をコメントアウトします。コメントを参照して理解することができます。

最初にCountDownLatchクラスのメインメソッドを
ここに写真の説明を挿入
確認し、次にそのクラス構造図を確認します
ここに写真の説明を挿入
。CountDownLatchクラスは、AQS抽象クラスを継承する内部クラスSyncが1つしかない独立したクラスです。前に述べたように、AQSはすべてのJUCロックの実装であり、ロックの基本的な操作を定義します。
ここでは、主await()countDown()2つのメソッドを分析します。
まず、CountDownLatchの構築メソッドを見てみましょう。

工法

public CountDownLatch(int count) {
    
    
//count不能小于0 ,否则会抛异常
 if (count < 0) throw new IllegalArgumentException("count < 0");
 	//调用内部类的构造方法
    this.sync = new Sync(count);
}

内部クラスSyncのメソッドの構築

 Sync(int count) {
    
    
 	//设置锁的state值
 	setState(count);
 }

親クラスAQSのメソッドを直接呼び出して、ロックの状態値を設定します。前述のように、この変数を制御することで、共有共有ロックまたは排他ロックを実現できます。

待つ()

public void await() throws InterruptedException {
    
    
  sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
    throws InterruptedException {
    
    
    //判断线程的中断标记,interrupted默认是false
    if (Thread.interrupted())
        throw new InterruptedException();
        //判断锁的state值,
        //等于0,需要把当前线程唤醒
    if (tryAcquireShared(arg) < 0)
    	//不等于0,需要去把当前线程挂起,阻塞
        doAcquireSharedInterruptibly(arg);
}

以前は、排他ロックを分析するときに、tryAcquire(arg)メソッドを使用してロックをプリエンプトしましたが、今回は、共有ロックはtryAcquireShared(arg)メソッドを使用します。

protected int tryAcquireShared(int acquires) {
    
    
   return (getState() == 0) ? 1 : -1;
}

クリックして、ロックの状態値を決定することを確認します。状態が0の場合、現在オンフックになっているスレッドがウェイクアップされることを意味します。0に等しくない場合、現在のスレッドがロックを取得できず、doAcquireSharedInterruptibly(arg)を呼び出しブロックしてハングする必要があることを意味します。

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException 
    //将当前线程的节点加到AQS队列的尾部
    //并返回这个节点,也是队列的最后一个节点
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
    
    
    	//自旋
        for (;;) {
    
    
        	//找到当前节点的上一个节点
            final Node p = node.predecessor();
            //如果上一个节点是head,说明前面已经没有线程阻挡它获得锁
            if (p == head) {
    
    
            	//去获得锁的state值
                int r = tryAcquireShared(arg);
                //表示state值为0
                if (r >= 0) {
    
    
                	//将当前节点变为head节点
                	//开始传播,并且一个接一个将后面的线程都唤醒
                    setHeadAndPropagate(node, r);
                    //将当前节点踢出去,帮助gc清理
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            //获取不到锁,就需要阻塞当前线程
            //阻塞之前,需要改变前一个节点的状态。如果是 SIGNAL 就阻塞,否则就改成 SIGNAL
            //这是为了提醒前一个节点释放锁完后能够叫醒自己
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
    
    
        if (failed)
            cancelAcquire(node);
    }
}

まず、addWaiterメソッドを使用して、現在のノードをAQSキューの最後に追加します。
現在のノードがヘッドノードで、ロックステータスが0の場合、setHeadAndPropagate()メソッドを実行してスレッドをウェイクアップします。

private void setHeadAndPropagate(Node node, int propagate) {
    
    
		//将当前节点变为head节点
        Node h = head; // Record old head for check below
        setHead(node);
       
       	//一个一个传播,将后面的线程全部唤醒
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
    
    
            Node s = node.next;
            //s==null是为了防止多线程操作时,再添加节点到为节点的时候,只来的及 node.prev = pred;
            //而 pred.next = node;还没执行的情况
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

ここでは、ウェイクアップ時の共有ロックと排他ロックの違いについて説明する必要があります。

  • 排他的ロック:ヘッドノードによってロックが取得されると、ヘッドノードのみがロックを取得し、他のノードのスレッドはスリープを継続し、ロックが解放されるのを待ってから次のノードのスレッドを起動します。
  • 共有ロック:ヘッドノードがロックを正常に取得している限り、AQSキュー内の次のノードのスレッドをウェイクアップし続けながら、自身のノードに対応するスレッドをウェイクアップします。各ノードは、自身をウェイクアップしながら、次のノードに対応するスレッドをウェイクアップします。 、共有状態の「逆伝播」を実現するために、共有機能を実現するために。

現在のノードがヘッドノードでない場合は、現在のスレッドをブロックして待機する必要があります。ここでのロジックReentrantLockのロジックと同じです。

  • 現在のノードの前のノードがヘッドノードである場合は、ロックの取得を試みることができます。
  • そうでない場合は、前のノードの状態をSIGNALに変更してから、ブロックして待機する必要があります。
    具体的な方法の分析については、のために、以前の記事を参照してReentrantLockのの深い理解。
    見てみるのawait方法、の実装カウントダウン方法はほぼ同じです。

秒読み()

public void countDown() {
    
    
   sync.releaseShared(1);
  }
  
public final boolean releaseShared(int arg) {
    
    
//将state值减1
 if (tryReleaseShared(arg)) {
    
    
 		//当state=0时执行
 		//去唤醒之前阻塞等待的线程
        doReleaseShared();
        return true;
    }
    return false;
}

CountDownLatchクラスのtryReleaseSharedメソッドの実装を見てください

protected boolean tryReleaseShared(int releases) {
    
    
	//防止CAS失败,自旋
     for (;;) {
    
    
     	//获取锁的state
         int c = getState();
         if (c == 0)
             return false;
         int nextc = c-1;
         if (compareAndSetState(c, nextc))
             return nextc == 0;
     }
 }

ロジックは非常に単純で、ロックの状態値を取得します

  • 開始状態の値が0の場合、falseを返します。
  • 1を引いた後に0の場合、trueを返します

状態が0になると、ブロックされた待機スレッドをウェイクアップする必要があります

private void doReleaseShared() {
    
    
 for (;;) {
    
    
     Node h = head;
     if (h != null && h != tail) {
    
    
         int ws = h.waitStatus;
         if (ws == Node.SIGNAL) {
    
    
             if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                 continue;            // loop to recheck cases
             unparkSuccessor(h);
         }
         else if (ws == 0 &&
                  !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
             continue;                // loop on failed CAS
     }
     if (h == head)                   // loop if head changed
         break;
 }
}

キュー内のヘッドノードがnullでなく、tailと等しくなく、状態がSIGNALである限り、状態はCASを介して0に変更され、成功すると、現在のスレッドが起動されます。現在のスレッドはdoAcquireSharedInterruptiblyメソッドでウェイクアップし、ロックの取得を再試行し、ドミノ効果と同様に、ロックの取得後も次のスレッドのウェイクアップを続行します。

総括する

  • CountDownLatchの内部カウンター値が0に達した後、その値は一定であり、awaitメソッドを実行し続けるスレッドは中断されません。
  • スレッドが永久に中断されるのを待つことを避けるために、countDown()メソッドは、finallyブロックなど、コードを常に実行できる場所に配置する必要があります。

おすすめ

転載: blog.csdn.net/xzw12138/article/details/106501919