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

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

前言

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

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表示計數已完成。

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.

2 CyclicBarrier

概念

實現讓所有線程全部等待彼此達到共同屏障點之後,才能同時執行。循環阻塞在涉及固定大小的線程方的程序中很有用,這些線程必須等待彼此,屏障又被稱爲循環 ,因爲它可以在等待的線程被釋放之後重新使用。

  • 可以簡單的理解爲CyclicBarrier是一個減1操作,CountDownLatch是加一操作。

常用api接口

方法 描述
CyclicBarrier(int parties) 創建一個新的 CyclicBarrier ,當給定數量的線程(線程)等待它,達到這個數量時,所有達到共同屏障昨天的線程都會繼續運行
CyclicBarrier(int parties, Runnable barrierAction) 類似於上面這種,只是多一個一個參數,可以理解爲行爲,也就是說,達到固定線程數後,就會執行runnable裏面的行爲代碼
await() 等待所有線程都已調用await方法
  • 這裏列舉一些常用的api接口,詳情的api接口,請查看jdk的api文檔

應用場景

  • 大部分的公司,每年都有一次聚會,公司會包一輛大巴車,而大巴車,需要等所有人都到達後,才能出發。

實戰

package com.cxyxs.thread.thirteen;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

/**
 * Description:
 * Author: 程序猿學社
 * Date:  2020/3/6 8:18
 * Modified By:
 */
public class CyclicBarrierTest {
    public static void main(String[] args) {
        final CyclicBarrier barrier = new CyclicBarrier(3, new Runnable() {
            @Override
            public void run() {
                //達到固定屏障數量後,就會觸發
                System.out.println("人員已到期,司機開始發車!");
            }
        });

        for (int i = 0; i < 3; i++) {
            final  int index =i;
            //使用lambda
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+",開始上車");
                try {
                    barrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+",上車完畢,當前時間:"+System.currentTimeMillis());
            },"員工"+i).start();
        }
    }
}

  • 模擬3個員工,他們公司旅遊,大巴司機發現人齊,就發車的一個場景
  • final關鍵字,注意Thread中是無法直接使用外面的i,所以引入一箇中間變量,用final修飾。
  • 使用了lambda表達式,實現一個線程,讓代碼更簡潔。
  • 發現每個員工上車後,就調用await方法,在等待其他的員工,人員一齊,司機就馬上發車。
  • 後面,還有一句話,上車完畢和當前時間,實際上也就是在調用await方法後,打印的一句話,爲什麼這樣設計? 實際上,就是更直觀的,讓我們知道,達到固定屏障數量後,所有的線程就會被喚醒,繼續運行,這也是我們概念裏面寫的同時執行
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章