在併發環境下,如果確定任務全部都完成了?
- 前提概念
- 背景
- 要實現的功能
- 要解決的問題
- 解決問題
- 分析問題
- 解決問題的方案
- 具體實現
- 通過CountDownLatch去實現
- 通過ThreadPoolExecutor.shutdown()和isTerminated()方法去實現
- 通過ThreadPoolExecutor.getActiveCount()去實現
前提概念
背景
在開發場景中,遇到了一個問題,前端調用我服務端的一個接口,會開啓一個耗時任務。但這任務正在執行的過程中,不允許有重複任務再被開啓,所以同時間內,只有允許有一個任務在執行,只有任務結束之後,才能接受再次的接口請求。因爲這個任務非常耗時,所以是用一個線程池做多線程的併發處理,大概十幾分鍾醬紫;所以我們的提供的接口必須是一個異步接口,不能讓請求一直阻塞着等待服務端處理完後的結果返回,因爲這樣會造成請求超時;當然我們也無法做到服務端處理完成後主動回調前端,因爲HTTP請求並不支持全雙工通信。所以架構師要求是當前端請求任務接口,必須做到如果有任務正在執行,則立即返回返回結果,msg : 有任務正在處理中
;如果任務執行完或沒有任務,則可以開啓任務,並立即返回結果,msg :正在處理
要實現的功能
接口A
用於開啓數據處理任務,前端請求服務端後,服務端開啓異步任務,無論能否執行,立即返回結果
要解決的問題
所以要做到這個功能,那麼我們就要先解決這麼幾個問題?
- 當有請求來襲,服務端怎麼知道有沒有任務正在進行?
- 當有請求來襲,服務端怎麼知道任務已經執行完畢,可以進行下一個任務?(
等價於我怎麼知道分配到線程池中的併發子任務都執行完了
)
解決問題
分析問題
- 問題一: 當有請求來襲,服務端怎麼知道有沒有任務正在進行?
- 問題二: 當有請求來襲,服務端怎麼知道任務已經執行完畢,可以進行下一個任務?
我們知道要實現上面的功能,那麼我們就要先解決上面的兩個問題;第一個問題很好解決,一個標誌位就可以做到了,比如用一個第三方緩存Redis去解決,當開啓任務時,更新Redis的任務狀態爲執行中
,當任務結束,更新Redis中的任務狀態爲結束
或刪除狀態
那麼問題來了,好像問題一的實現也是依賴問題二的實現,即我要知道併發任務什麼時候被處理完,才能更新任務已完成的狀態呀。哈哈,這就回到了標題上的問題啦!到底要怎麼知道呢?主線程接到任務通知後,就把每一個子任務都扔進線程池中了,主線程根本就不管我到底是什麼時候開始執行,什麼時候執行完,涼涼,感覺很苦惱的樣子~
解決問題的方案
經過我在網上的查找, 也是發現了一些可行的方案的,所以就在這裏跟大家分享一下
- 通過
CountDownLatch
去實現 - 通過ThreadPoolExecutor.
shutdown()
和isTerminated()
方法去實現 - 通過ThreadPoolExecutor.
getActiveCount()
去實現
具體實現
通過CountDownLatch去實現
public class TaskisDoneTest {
public static void main(String[] args) throws InterruptedException {
countDownLatchTest(createExecutor());
}
private static void countDownLatchTest(ThreadPoolExecutor executor) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1000);
AtomicInteger count = new AtomicInteger(0);
for (int i = 0; i < 1000; i++) {
executor.execute(() -> {
//一定要對子任務的主邏輯進行try/catch, 保證countDownLatch每個子任務執行後都要countDown
try {
doTask(count);
} catch (Exception e) {
e.printStackTrace();
} finally {
//不管任務是否執行成功,那麼都要countDown,避免主線程被永遠阻塞或到達超時時間
countDownLatch.countDown();
}
});
}
//主線程阻塞,直至countDownLatch減至0,或超過超時時間
countDownLatch.await(1, TimeUnit.HOURS);
System.out.println("任務執行完畢");
}
/**
* 默認耗時子任務
*
* @param count
*/
private static void doTask(AtomicInteger count) throws InterruptedException {
System.out.println(count.incrementAndGet() + "號子任務正在執行中...");
Thread.sleep(100);
}
/**
* 獲得線程池
*
*/
private static ThreadPoolExecutor createExecutor() {
//獲取系統核心數
int coreSize = Runtime.getRuntime().availableProcessors();
int maxSize = coreSize * 4;
return new ThreadPoolExecutor(coreSize,
maxSize,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(maxSize),
new ThreadPoolExecutor.CallerRunsPolicy());
}
}
CountDownLatch
的大小要按子任務的數量來定,比如有1000
個子任務,那就需要將CountDownLatch
的大小初始化爲1000
- 子線程獲取
CountDownLatch
,每次執行完任務後就countDown()
, 相當於-1
操作 - 但要注意的是,子線程操作
CountDownLatch
時,要特別關注,任務是可能會出現異常情況的,所以通常我們要將子線程主邏輯try/catch/finally
起來,將countDown()
操作放到finally
中。這樣可以做到即使子任務出現異常,也可以正確執行countDown()
操作,避免造成死鎖 - 主線程將所有子任務拋進線程池後,就可以執行
await()
等待,爲了安全起見,我們最好先預估整個併發任務的時間,給予主線程最大的超時等待時間,避免出現死鎖
通過ThreadPoolExecutor.shutdown()和isTerminated()方法去實現
public class TaskisDoneTest {
public static void main(String[] args) throws InterruptedException {
executorShutDownTest(createExecutor());
}
private static void executorShutDownTest(ThreadPoolExecutor executor) throws InterruptedException {
AtomicInteger count = new AtomicInteger(0);
for (int i = 0; i < 1000; i++) {
executor.execute(() -> {
try {
doTask(count);
} catch (Exception e) {
e.getStackTrace();
}
});
}
//關閉線程池,線程池發出關閉指令,不再接收新的任務,但不會立即關閉,會將剩餘任務執行完後再關閉(在阻塞隊列中的任就會執行的)
executor.shutdown();
while (true) {
//判斷線程池是否已關閉,如果已關閉,就代表所有任務執行完畢了
if (executor.isTerminated()) {
System.out.println("任務執行完畢");
break;
}
//每次判斷休眠200毫秒,避免長時間空轉
Thread.sleep(200);
}
}
/**
* 默認耗時子任務
*
* @param count
*/
private static void doTask(AtomicInteger count) throws InterruptedException {
System.out.println(count.incrementAndGet() + "號子任務正在執行中...");
Thread.sleep(100);
}
private static ThreadPoolExecutor createExecutor() {
//獲取系統核心數
int coreSize = Runtime.getRuntime().availableProcessors();
int maxSize = coreSize * 4;
return new ThreadPoolExecutor(coreSize,
maxSize,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(maxSize),
new ThreadPoolExecutor.CallerRunsPolicy());
}
}
原理:
- 當我們對線程池執行shutdown()操作了之後,線程池的阻塞隊列就不會再接受新的任務了,正在執行任務的線程沒有執行完,且在阻塞隊列中的任務沒有被線程執行完前,線程池依然是不會關閉的。只有線程池中所有的線程都執行完畢,阻塞隊列已無任務的時候,線程池纔會得以關閉。所有線程池是可以感知被提交到池中的任務是什麼時候被全部執行完的
- 當我們對線程池執行isTerminated()操作時,是可以感知線程池是否已經關閉,如果關閉就代表所有被投遞到線程池中的任務都已經被執行完了。
代碼:
- 在所有的子任務都被投遞到線程池後,就可以開始對線程池進行
shutdown
操作了,但注意的是,不是shutdownNow
操作,它們兩者是不一樣的 - 當線程池執行完
shutdown
操作後,我們就可以輪詢主線程,不斷判斷線程池是否已經關閉,isTerminated()
通過ThreadPoolExecutor.getActiveCount()去實現
public class TaskisDoneTest {
public static void main(String[] args) throws InterruptedException {
executorActiveTest(createExecutor());
}
private static void executorActiveTest(ThreadPoolExecutor executor) throws InterruptedException {
AtomicInteger count = new AtomicInteger(0);
//將1000個子任務投遞到線程池中
for (int i = 0; i < 1000; i++) {
executor.execute(() -> {
try {
doTask(count);
} catch (Exception e) {
e.getStackTrace();
}
});
}
while (true){
//線程池中的活躍數是否爲0,
if (executor.getActiveCount() == 0){
System.out.println("任務執行完畢");
break;
}
//每次判斷休眠200毫秒,避免長時間空轉
Thread.sleep(200);
}
}
/**
* 默認耗時子任務
*
* @param count
*/
private static void doTask(AtomicInteger count) throws InterruptedException {
System.out.println(count.incrementAndGet() + "號子任務正在執行中...");
Thread.sleep(100);
}
private static ThreadPoolExecutor createExecutor() {
//獲取系統核心數
int coreSize = Runtime.getRuntime().availableProcessors();
int maxSize = coreSize * 4;
return new ThreadPoolExecutor(coreSize,
maxSize,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(maxSize),
new ThreadPoolExecutor.CallerRunsPolicy());
}
原理:
- 線程池的getActiveCount操作可以獲知,線程池當前正活躍中的線程個數(近似值);如果線程池中的活躍線程數趨近於0,就說明投遞到線程池中的子任務都已經被執行完了
- Returns the approximate number of threads that are actively executing tasks.
代碼:
- 先將所有的子任務都投遞到線程池中,然後主線程輪詢判斷線程池中的活躍子線程是否趨近於0,如果是就代表所有子任務已完成
小結
- CountDownLatch的方式比較常見,不失爲一種控制線程邏輯的好方式,不過要是操作不當,容易操作線程死鎖的問題。
- 線程池shutdown的方式比較簡單,只需要等待線程池關閉即可,實現簡單,每次的請求都會創建一個新的線程池去執行任務,然後在關閉它,具有一定的線程池內存消耗
- 線程池getActiveCount的方法的返回值是一個近似值,只能保證一個大約情況。因爲自己沒有這麼使用過,而且可能會存在其他影響因子動態的變化,所以這個方法不是太推薦,但是在一定程度上也可以做到。重點是它實現簡單,而已不需要每次都重建線程池