Tiefes Verständnis von CountDownLatch

Konzept

CountDownLatch kann verwendet werden, um einen oder mehrere Threads zu implementieren, die darauf warten, dass andere Threads eine Reihe bestimmter Vorgänge ausführen, bevor sie weiter ausgeführt werden. Diese Gruppe von Operationen wird als vorausgesetzte Operationen bezeichnet

verwenden

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时唤醒
}

Betriebsergebnis:

Fügen Sie hier eine Bildbeschreibung ein
CountDownLatch verwaltet intern einen Zähler , der die Anzahl der ausstehenden Vorgänge darstellt . CountDownLatch.countDown () verringert den Zählerwert der entsprechenden Instanz bei jeder Ausführung um eins.
CountDownLatch.await () ist eine Blockierungsmethode. Wenn der Zählerwert 0 ist, bedeutet dies, dass alle erforderlichen Operationen ausgeführt wurden und der blockierte Thread aktiviert wird.

Quellcode-Analyse

Hinweis: Ich werde die Funktion jeder Methode im Quellcode auskommentieren. Sie können sich zum Verständnis auf die Kommentare beziehen.

Schauen Sie sich zuerst die Hauptmethoden der CountDownLatch-Klasse
Fügen Sie hier eine Bildbeschreibung ein
und dann das Klassenstrukturdiagramm an. Die
Fügen Sie hier eine Bildbeschreibung ein
CountDownLatch-Klasse ist eine unabhängige Klasse mit nur einer internen Klasse Sync, die die abstrakte AQS-Klasse erbt. Wie bereits erwähnt, ist AQS die Implementierung aller JUC-Sperren und definiert die grundlegende Funktionsweise von Sperren.
Hier analysieren wir hauptsächlich die beiden Methoden von await () und countDown () .
Schauen wir uns zunächst die Konstruktionsmethode von CountDownLatch an.

Konstruktionsmethode

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

Konstruktionsmethode der inneren Klasse Sync

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

Rufen Sie direkt die Methode der übergeordneten Klasse AQS auf, um den Statuswert der Sperre festzulegen. Wie bereits erwähnt, kann durch Steuern dieser Variablen eine gemeinsame gemeinsame Sperre oder eine exklusive Sperre realisiert werden.

erwarten()

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);
}

Früher haben wir bei der Analyse exklusiver Sperren die Methode tryAcquire (arg) verwendet, um die Sperre zu verhindern. Diesmal erfolgt die gemeinsame Sperre jedoch über die Methode tryAcquireShared (arg) .

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

Klicken Sie hier, um den Statuswert der Sperre zu ermitteln. Wenn der Status 0 ist, bedeutet dies, dass der Thread, der gerade aufgelegt ist , aktiviert wird. Wenn er nicht gleich 0 ist, bedeutet dies, dass der aktuelle Thread die Sperre nicht erhalten kann und doAcquireSharedInterruptibly (arg) aufrufen muss, um zu blockieren und zu hängen

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);
    }
}

Fügen Sie zuerst den aktuellen Knoten über die addWaiter- Methode am Ende der AQS-Warteschlange hinzu .
Wenn der aktuelle Knoten der Kopfknoten ist und der Sperrstatus 0 ist, führen Sie die Methode setHeadAndPropagate () aus, um den Thread zu aktivieren

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();
        }
    }

Hier müssen wir über den Unterschied zwischen einer gemeinsam genutzten Sperre und einer exklusiven Sperre beim Aufwachen sprechen:

  • Exklusive Sperre: Wenn die Sperre vom Kopfknoten erworben wird, erhält nur der Kopfknoten die Sperre, und die Threads der verbleibenden Knoten schlafen weiter und warten darauf, dass die Sperre aufgehoben wird, bevor der Thread des nächsten Knotens aktiviert wird.
  • Gemeinsame Sperre: Solange der Kopfknoten die Sperre erfolgreich erlangt, wird der Thread, der seinem eigenen Knoten entspricht, aktiviert, während der Thread des nächsten Knotens in der AQS-Warteschlange weiter aktiviert wird. Jeder Knoten aktiviert den Thread, der dem nächsten Knoten entspricht, während er sich selbst aufweckt Um die "Rückwärtsausbreitung" des gemeinsamen Zustands zu realisieren, wodurch die gemeinsame Funktion realisiert wird.

Wenn der aktuelle Knoten nicht der Kopfknoten ist, muss er den aktuellen Thread blockieren und auf ihn warten. Die Logik hier ist dieselbe wie die ReentrantLock- Logik:

  • Wenn der vorherige Knoten des aktuellen Knotens der Kopfknoten ist, können Sie versuchen, die Sperre zu erhalten.
  • Wenn nicht, müssen Sie den Status des vorherigen Knotens in SIGNAL ändern und dann blockieren und warten.
    Informationen zur spezifischen Methodenanalyse finden Sie im vorherigen Artikel, um ein umfassendes Verständnis von ReentrantLock zu erhalten.
    Bei der Methode await ist die Implementierung der countDown- Methode nahezu identisch.

Countdown()

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

Sehen Sie sich die Implementierung der tryReleaseShared- Methode der CountDownLatch-Klasse an

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;
     }
 }

Die Logik ist sehr einfach, erhalten Sie den Statuswert der Sperre

  • Wenn der Wert des Startzustands 0 ist, geben Sie false zurück.
  • Wenn es nach Subtraktion von 1 0 ist, wird true zurückgegeben

Wenn der Status auf 0 reduziert wird, muss der blockierende wartende Thread aktiviert werden

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;
 }
}

Solange der Kopfknoten in der Warteschlange nicht null ist und nicht gleich tail ist und der Status SIGNAL ist , wird der Status durch CAS auf 0 geändert. Wenn dies erfolgreich ist, wird der aktuelle Thread aktiviert. Der aktuelle Thread wird in der Methode doAcquireSharedInterruptibly aktiviert, versucht erneut, die Sperre zu erhalten, und aktiviert den nächsten Thread nach dem Erwerb der Sperre wie bei einem Dominoeffekt weiter.

um zusammenzufassen

  • Nachdem der interne Zählerwert von CountDownLatch 0 erreicht hat, ist sein Wert konstant, und jeder Thread, der die Methode await weiterhin ausführt, wird nicht angehalten
  • Um zu vermeiden, dass der Thread für immer angehalten wird, muss die countDown () -Methode dort platziert werden, wo der Code immer ausgeführt werden kann, z. B. im finally-Block

Ich denke du magst

Origin blog.csdn.net/xzw12138/article/details/106501919
Empfohlen
Rangfolge