【多線程併發編程】十三 CountDownLatch

程序猿學社的GitHub,歡迎Star
https://github.com/ITfqyd/cxyxs
本文已記錄到github,形成對應專題。

前言

jdk1.5以後,增加了不少內容,我們就來看一看CountDownLatch,實際上,很多的方法,如果實際開發過程中,沒有用到過,我們幾乎都不怎麼熟悉。一切的一切,都感覺十分的陌生。

1.CountDownLatch

概念

允許一個或多個線程等待直到在其他線程執行完畢後再執行。

  • CountDownLatch用給定的計數初始化
  • await方法阻塞
  • 直到countDown()方法的調用而導致當前計數達到零,之後所有等待線程被釋放,並且任何後續的await 調用立即返回。

常用api接口

方法 描述
await() 導致當前線程等到鎖存器計數到零,除非線程是 interrupted 。線程處於休眠狀態,直到計數爲0
countDown() 減少鎖存器的計數(調用一次,計數減去1),如果計數達到零,釋放所有等待的線程。

應用場景

現在是疫情期間,爲了保證員工的安全,減少與他們接觸的機會,某某公司,實行車接車送。所有人員就位後,司機需要向HR彙報,人員上車情況。

實戰

demo案例

package com.cxyxs.thread.thirteen;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * Description:轉發請註明來源  程序猿學社 - https://ithub.blog.csdn.net/
 * Author: 程序猿學社
 * Date:  2020/3/3 16:24
 * Modified By:
 */
public class Emp {
      //初始化爲3,標識有3個員工需要等待
      public static CountDownLatch countDownLatch = new CountDownLatch(3);

    /**
     * 員工正在上車中
     */
    public void toAddress(){
       System.out.println(Thread.currentThread().getName()+"正在步行去固定的上車地點...");
       //模擬去固定地點上車的過程
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        countDownLatch.countDown();
        System.out.println(Thread.currentThread().getName()+"已上車");
    }
}

調用類

package com.cxyxs.thread.thirteen;

/**
 * Description:轉發請註明來源  程序猿學社 - https://ithub.blog.csdn.net/
 * Author: 程序猿學社
 * Date:  2020/3/3 16:22
 * Modified By:
 */
public class Demo1 {
    public static void main(String[] args) {
        Emp emp = new Emp();
        //使用lambda方式實現一個線程
        new Thread(()->{
            emp.toAddress();
        },"張三").start();
        new Thread(()->{
            emp.toAddress();
        },"李四").start();
        new Thread(()->{
            emp.toAddress();
        },"王五").start();


        new Thread(()->{
            try {
                emp.countDownLatch.await();
                System.out.println(Thread.currentThread().getName()+"向HR彙報上車情況!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"黃師傅").start();
    }
}

  • 初始化計數爲3,調用countDown方法後,每次減去1,一直到零,就會主動釋放,繼續await後面的代碼。
  • 在實際使用過程中,一定要記得調用一次,就調用countDown方法,不然,線程會一直堵塞在哪裏。

源碼分析

CountDownLatch初始化

 public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
}
  • 初始化一個CountDownLatch對象,需要給定計數的初始值,如果<0,則會拋出異常。
  • 創建了一個Sync對象。
 /**
     * Synchronization control For CountDownLatch.
     * Uses AQS state to represent count.
     */
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

        Sync(int count) {
            setState(count);
        }
        protected final void setState(int newState) {
        state = newState;
        }
    }
    private volatile int state;
    private final Sync sync;
  • 首先調用Sync的構造方法。該構造方法會調用setState方法。
  • sync繼承AbstractQueuedSynchronizer抽象類,其中有一個變量state,就是爲了同步狀態的。注意,他的前面有volatile關鍵字,表示線程之間是可見的,只要值有變動,其他的項目都會知道。類似於開發過程中,某某提交了代碼,但是,不說明,其他的人,只有更新代碼的時候,發現起衝突才知道,改了同一個文件,而可見性,就是,某人一改代碼,馬上,就全網通知,說我改代碼了,你們都拉取一下最新的代碼。

countDown方法

  public void countDown() {
        sync.releaseShared(1);
    }
  • 調用AbstractQueuedSynchronizer方法
 public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
  • 調用了兩個方法tryReleaseShared和doReleaseShared
 protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
  • 獲取state的數量,也就是計數器的值,如果爲0直接返回false。表示計數已經完成。
  • compareAndSetState方法我們一步步跟蹤下去,發現調用的compareAndSwapInt被native關鍵字修飾,說明是調用的事c或者c++的方法。利用了CAS,可以理解爲樂觀鎖, 但是,可以保證該變量是原子的。代碼塊,就無法保證了。說到原子,我們就可以聯想起多個線程操作i++線程不安全的問題。這也是一到面試經常會的題目。
  • -1完後,再次檢查是否爲0,如果爲0表示計數已完成。
  private void doReleaseShared() {
        /*
         * Ensure that a release propagates, even if there are other
         * in-progress acquires/releases.  This proceeds in the usual
         * way of trying to unparkSuccessor of head if it needs
         * signal. But if it does not, status is set to PROPAGATE to
         * ensure that upon release, propagation continues.
         * Additionally, we must loop in case a new node is added
         * while we are doing this. Also, unlike other uses of
         * unparkSuccessor, we need to know if CAS to reset status
         * fails, if so rechecking.
         */
        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;
        }
    }

  • 在jdk1.8的api文檔中,沒有找到這個方法,但是jdk1.8源碼裏面有這個方法。後續再花時間看看把。

await方法

public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
}
  • 實際上是調用父類的acquireSharedInterruptibly方法
 public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }
  • 判斷當前線程是否中斷,如果中斷則拋出InterruptedException異常。
  • tryAcquireShared的返回值小於纔會調用doAcquireSharedInterruptibly方法
tryAcquireShared代碼
   protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }
  • 判斷state的計算是否爲0,如果爲0返回1.
doAcquireSharedInterruptiblyd源碼
 /**
     * Acquires in shared interruptible mode.
     * @param arg the acquire argument
     */
    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        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
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
  • 看着也是一頭霧水。
    總結:
    就一個簡單的方法調用,實際上底層幫我們做了很多的工作,裏面層層的調用,實際上,需要理解的很透徹,還的需要花大量的時間。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章