程序猿學社的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方法後,打印的一句話,爲什麼這樣設計? 實際上,就是更直觀的,讓我們知道,達到固定屏障數量後,所有的線程就會被喚醒,繼續運行,這也是我們概念裏面寫的同時執行。