這是java高併發系列第16篇文章。
本篇內容
- 介紹CountDownLatch及使用場景
- 提供幾個示例介紹CountDownLatch的使用
- 手寫一個並行處理任務的工具類
假如有這樣一個需求,當我們需要解析一個Excel裡多個sheet的資料時,可以考慮使用多執行緒,每個執行緒解析一個sheet裡的資料,等到所有的sheet都解析完之後,程式需要統計解析總耗時。分析一下:解析每個sheet耗時可能不一樣,總耗時就是最長耗時的那個操作。
我們能夠想到的最簡單的做法是使用join,程式碼如下:
package com.itsoku.chat13;
import java.util.concurrent.TimeUnit;
/**
* 微信公眾號:javacode2018,獲取年薪50萬課程
*/
public class Demo1 {
public static class T extends Thread {
//休眠時間(秒)
int sleepSeconds;
public T(String name, int sleepSeconds) {
super(name);
this.sleepSeconds = sleepSeconds;
}
@Override
public void run() {
Thread ct = Thread.currentThread();
long startTime = System.currentTimeMillis();
System.out.println(startTime + "," + ct.getName() + ",開始處理!");
try {
//模擬耗時操作,休眠sleepSeconds秒
TimeUnit.SECONDS.sleep(this.sleepSeconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println(endTime + "," + ct.getName() + ",處理完畢,耗時:" + (endTime - startTime));
}
}
public static void main(String[] args) throws InterruptedException {
long starTime = System.currentTimeMillis();
T t1 = new T("解析sheet1執行緒", 2);
t1.start();
T t2 = new T("解析sheet2執行緒", 5);
t2.start();
t1.join();
t2.join();
long endTime = System.currentTimeMillis();
System.out.println("總耗時:" + (endTime - starTime));
}
}
輸出:
1563767560271,解析sheet1執行緒,開始處理!
1563767560272,解析sheet2執行緒,開始處理!
1563767562273,解析sheet1執行緒,處理完畢,耗時:2002
1563767565274,解析sheet2執行緒,處理完畢,耗時:5002
總耗時:5005
程式碼中啟動了2個解析sheet的執行緒,第一個耗時2秒,第二個耗時5秒,最終結果中總耗時:5秒。上面的關鍵技術點是執行緒的join()
方法,此方法會讓當前執行緒等待被呼叫的執行緒完成之後才能繼續。可以看一下join的原始碼,內部其實是在synchronized方法中呼叫了執行緒的wait方法,最後被呼叫的執行緒執行完畢之後,由jvm自動呼叫其notifyAll()方法,喚醒所有等待中的執行緒。這個notifyAll()方法是由jvm內部自動呼叫的,jdk原始碼中是看不到的,需要看jvm原始碼,有興趣的同學可以去查一下。所以JDK不推薦線上程上呼叫wait、notify、notifyAll方法。
而在JDK1.5之後的併發包中提供的CountDownLatch也可以實現join的這個功能。
CountDownLatch介紹
CountDownLatch稱之為閉鎖,它可以使一個或一批執行緒在閉鎖上等待,等到其他執行緒執行完相應操作後,閉鎖開啟,這些等待的執行緒才可以繼續執行。確切的說,閉鎖在內部維護了一個倒計數器。通過該計數器的值來決定閉鎖的狀態,從而決定是否允許等待的執行緒繼續執行。
常用方法:
public CountDownLatch(int count):構造方法,count表示計數器的值,不能小於0,否者會報異常。
public void await() throws InterruptedException:呼叫await()會讓當前執行緒等待,直到計數器為0的時候,方法才會返回,此方法會響應執行緒中斷操作。
public boolean await(long timeout, TimeUnit unit) throws InterruptedException:限時等待,在超時之前,計數器變為了0,方法返回true,否者直到超時,返回false,此方法會響應執行緒中斷操作。
public void countDown():讓計數器減1
CountDownLatch使用步驟:
- 建立CountDownLatch物件
- 呼叫其例項方法
await()
,讓當前執行緒等待 - 呼叫
countDown()
方法,讓計數器減1 - 當計數器變為0的時候,
await()
方法會返回
示例1:一個簡單的示例
我們使用CountDownLatch來完成上面示例中使用join實現的功能,程式碼如下:
package com.itsoku.chat13;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* 微信公眾號:javacode2018,獲取年薪50萬課程
*/
public class Demo2 {
public static class T extends Thread {
//休眠時間(秒)
int sleepSeconds;
CountDownLatch countDownLatch;
public T(String name, int sleepSeconds, CountDownLatch countDownLatch) {
super(name);
this.sleepSeconds = sleepSeconds;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
Thread ct = Thread.currentThread();
long startTime = System.currentTimeMillis();
System.out.println(startTime + "," + ct.getName() + ",開始處理!");
try {
//模擬耗時操作,休眠sleepSeconds秒
TimeUnit.SECONDS.sleep(this.sleepSeconds);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
long endTime = System.currentTimeMillis();
System.out.println(endTime + "," + ct.getName() + ",處理完畢,耗時:" + (endTime - startTime));
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + "執行緒 start!");
CountDownLatch countDownLatch = new CountDownLatch(2);
long starTime = System.currentTimeMillis();
T t1 = new T("解析sheet1執行緒", 2, countDownLatch);
t1.start();
T t2 = new T("解析sheet2執行緒", 5, countDownLatch);
t2.start();
countDownLatch.await();
System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + "執行緒 end!");
long endTime = System.currentTimeMillis();
System.out.println("總耗時:" + (endTime - starTime));
}
}
輸出:
1563767580511,main執行緒 start!
1563767580513,解析sheet1執行緒,開始處理!
1563767580513,解析sheet2執行緒,開始處理!
1563767582515,解析sheet1執行緒,處理完畢,耗時:2002
1563767585515,解析sheet2執行緒,處理完畢,耗時:5002
1563767585515,main執行緒 end!
總耗時:5003
從結果中看出,效果和join實現的效果一樣,程式碼中建立了計數器為2的CountDownLatch
,主執行緒中呼叫countDownLatch.await();
會讓主執行緒等待,t1、t2執行緒中模擬執行耗時操作,最終在finally中呼叫了countDownLatch.countDown();
,此方法每呼叫一次,CountDownLatch內部計數器會減1,當計數器變為0的時候,主執行緒中的await()會返回,然後繼續執行。注意:上面的countDown()
這個是必須要執行的方法,所以放在finally中執行。
示例2:等待指定的時間
還是上面的示例,2個執行緒解析2個sheet,主執行緒等待2個sheet解析完成。主執行緒說,我等待2秒,你們還是無法處理完成,就不等待了,直接返回。如下程式碼:
package com.itsoku.chat13;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* 微信公眾號:javacode2018,獲取年薪50萬課程
*/
public class Demo3 {
public static class T extends Thread {
//休眠時間(秒)
int sleepSeconds;
CountDownLatch countDownLatch;
public T(String name, int sleepSeconds, CountDownLatch countDownLatch) {
super(name);
this.sleepSeconds = sleepSeconds;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
Thread ct = Thread.currentThread();
long startTime = System.currentTimeMillis();
System.out.println(startTime + "," + ct.getName() + ",開始處理!");
try {
//模擬耗時操作,休眠sleepSeconds秒
TimeUnit.SECONDS.sleep(this.sleepSeconds);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
long endTime = System.currentTimeMillis();
System.out.println(endTime + "," + ct.getName() + ",處理完畢,耗時:" + (endTime - startTime));
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + "執行緒 start!");
CountDownLatch countDownLatch = new CountDownLatch(2);
long starTime = System.currentTimeMillis();
T t1 = new T("解析sheet1執行緒", 2, countDownLatch);
t1.start();
T t2 = new T("解析sheet2執行緒", 5, countDownLatch);
t2.start();
boolean result = countDownLatch.await(2, TimeUnit.SECONDS);
System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + "執行緒 end!");
long endTime = System.currentTimeMillis();
System.out.println("主執行緒耗時:" + (endTime - starTime) + ",result:" + result);
}
}
輸出:
1563767637316,main執行緒 start!
1563767637320,解析sheet1執行緒,開始處理!
1563767637320,解析sheet2執行緒,開始處理!
1563767639321,解析sheet1執行緒,處理完畢,耗時:2001
1563767639322,main執行緒 end!
主執行緒耗時:2004,result:false
1563767642322,解析sheet2執行緒,處理完畢,耗時:5002
從輸出結果中可以看出,執行緒2耗時了5秒,主執行緒耗時了2秒,主執行緒中呼叫countDownLatch.await(2, TimeUnit.SECONDS);
,表示最多等2秒,不管計數器是否為0,await方法都會返回,若等待時間內,計數器變為0了,立即返回true,否則超時後返回false。
示例3:2個CountDown結合使用的示例
有3個人參見跑步比賽,需要先等指令員發指令槍後才能開跑,所有人都跑完之後,指令員喊一聲,大家跑完了。
示例程式碼:
package com.itsoku.chat13;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* 微信公眾號:javacode2018,獲取年薪50萬課程
*/
public class Demo4 {
public static class T extends Thread {
//跑步耗時(秒)
int runCostSeconds;
CountDownLatch commanderCd;
CountDownLatch countDown;
public T(String name, int runCostSeconds, CountDownLatch commanderCd, CountDownLatch countDown) {
super(name);
this.runCostSeconds = runCostSeconds;
this.commanderCd = commanderCd;
this.countDown = countDown;
}
@Override
public void run() {
//等待指令員槍響
try {
commanderCd.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread ct = Thread.currentThread();
long startTime = System.currentTimeMillis();
System.out.println(startTime + "," + ct.getName() + ",開始跑!");
try {
//模擬耗時操作,休眠runCostSeconds秒
TimeUnit.SECONDS.sleep(this.runCostSeconds);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDown.countDown();
}
long endTime = System.currentTimeMillis();
System.out.println(endTime + "," + ct.getName() + ",跑步結束,耗時:" + (endTime - startTime));
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + "執行緒 start!");
CountDownLatch commanderCd = new CountDownLatch(1);
CountDownLatch countDownLatch = new CountDownLatch(3);
long starTime = System.currentTimeMillis();
T t1 = new T("小張", 2, commanderCd, countDownLatch);
t1.start();
T t2 = new T("小李", 5, commanderCd, countDownLatch);
t2.start();
T t3 = new T("路人甲", 10, commanderCd, countDownLatch);
t3.start();
//主執行緒休眠5秒,模擬指令員準備發槍耗時操作
TimeUnit.SECONDS.sleep(5);
System.out.println(System.currentTimeMillis() + ",槍響了,大家開始跑");
commanderCd.countDown();
countDownLatch.await();
long endTime = System.currentTimeMillis();
System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + "所有人跑完了,主執行緒耗時:" + (endTime - starTime));
}
}
輸出:
1563767691087,main執行緒 start!
1563767696092,槍響了,大家開始跑
1563767696092,小張,開始跑!
1563767696092,小李,開始跑!
1563767696092,路人甲,開始跑!
1563767698093,小張,跑步結束,耗時:2001
1563767701093,小李,跑步結束,耗時:5001
1563767706093,路人甲,跑步結束,耗時:10001
1563767706093,main所有人跑完了,主執行緒耗時:15004
程式碼中,t1、t2、t3啟動之後,都阻塞在commanderCd.await();
,主執行緒模擬發槍準備操作耗時5秒,然後呼叫commanderCd.countDown();
模擬發槍操作,此方法被呼叫以後,阻塞在commanderCd.await();
的3個執行緒會向下執行。主執行緒呼叫countDownLatch.await();
之後進行等待,每個人跑完之後,呼叫countDown.countDown();
通知一下countDownLatch
讓計數器減1,最後3個人都跑完了,主執行緒從countDownLatch.await();
返回繼續向下執行。
手寫一個並行處理任務的工具類
package com.itsoku.chat13;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 微信公眾號:javacode2018,獲取年薪50萬課程
*/
public class TaskDisposeUtils {
//並行執行緒數
public static final int POOL_SIZE;
static {
POOL_SIZE = Integer.max(Runtime.getRuntime().availableProcessors(), 5);
}
/**
* 並行處理,並等待結束
*
* @param taskList 任務列表
* @param consumer 消費者
* @param <T>
* @throws InterruptedException
*/
public static <T> void dispose(List<T> taskList, Consumer<T> consumer) throws InterruptedException {
dispose(true, POOL_SIZE, taskList, consumer);
}
/**
* 並行處理,並等待結束
*
* @param moreThread 是否多執行緒執行
* @param poolSize 執行緒池大小
* @param taskList 任務列表
* @param consumer 消費者
* @param <T>
* @throws InterruptedException
*/
public static <T> void dispose(boolean moreThread, int poolSize, List<T> taskList, Consumer<T> consumer) throws InterruptedException {
if (CollectionUtils.isEmpty(taskList)) {
return;
}
if (moreThread && poolSize > 1) {
poolSize = Math.min(poolSize, taskList.size());
ExecutorService executorService = null;
try {
executorService = Executors.newFixedThreadPool(poolSize);
CountDownLatch countDownLatch = new CountDownLatch(taskList.size());
for (T item : taskList) {
executorService.execute(() -> {
try {
consumer.accept(item);
} finally {
countDownLatch.countDown();
}
});
}
countDownLatch.await();
} finally {
if (executorService != null) {
executorService.shutdown();
}
}
} else {
for (T item : taskList) {
consumer.accept(item);
}
}
}
public static void main(String[] args) throws InterruptedException {
//生成1-10的10個數字,放在list中,相當於10個任務
List<Integer> list = Stream.iterate(1, a -> a + 1).limit(10).collect(Collectors.toList());
//啟動多執行緒處理list中的資料,每個任務休眠時間為list中的數值
TaskDisposeUtils.dispose(list, item -> {
try {
long startTime = System.currentTimeMillis();
TimeUnit.SECONDS.sleep(item);
long endTime = System.currentTimeMillis();
System.out.println(System.currentTimeMillis() + ",任務" + item + "執行完畢,耗時:" + (endTime - startTime));
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//上面所有任務處理完畢完畢之後,程式才能繼續
System.out.println(list + "中的任務都處理完畢!");
}
}
執行程式碼輸出:
1563769828130,任務1執行完畢,耗時:1000
1563769829130,任務2執行完畢,耗時:2000
1563769830131,任務3執行完畢,耗時:3001
1563769831131,任務4執行完畢,耗時:4001
1563769832131,任務5執行完畢,耗時:5001
1563769833130,任務6執行完畢,耗時:6000
1563769834131,任務7執行完畢,耗時:7001
1563769835131,任務8執行完畢,耗時:8001
1563769837131,任務9執行完畢,耗時:9001
1563769839131,任務10執行完畢,耗時:10001
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]中的任務都處理完畢!
TaskDisposeUtils是一個並行處理的工具類,可以傳入n個任務內部使用執行緒池進行處理,等待所有任務都處理完成之後,方法才會返回。比如我們傳送簡訊,系統中有1萬條簡訊,我們使用上面的工具,每次取100條並行傳送,待100個都處理完畢之後,再取一批按照同樣的邏輯傳送。
java高併發系列
- java高併發系列 - 第1天:必須知道的幾個概念
- java高併發系列 - 第2天:併發級別
- java高併發系列 - 第3天:有關並行的兩個重要定律
- java高併發系列 - 第4天:JMM相關的一些概念
- java高併發系列 - 第5天:深入理解程式和執行緒
- java高併發系列 - 第6天:執行緒的基本操作
- java高併發系列 - 第7天:volatile與Java記憶體模型
- java高併發系列 - 第8天:執行緒組
- java高併發系列 - 第9天:使用者執行緒和守護執行緒
- java高併發系列 - 第10天:執行緒安全和synchronized關鍵字
- java高併發系列 - 第11天:執行緒中斷的幾種方式
- java高併發系列 - 第12天JUC:ReentrantLock重入鎖
- java高併發系列 - 第13天:JUC中的Condition物件
- java高併發系列 - 第14天:JUC中的LockSupport工具類,必備技能
- java高併發系列 - 第15天:JUC中的Semaphore(訊號量)
java高併發系列連載中,總計估計會有四五十篇文章,可以關注公眾號:javacode2018,送年薪50萬課程,獲取最新文章。