1. 簡介
- CountDownLatch 是一個同步工具類,它允許一個或多個線程一直等待,直到其他線程執行完後再執行。
- CountDownLatch 在 JDK 1.5 被引入。
- 存在於 java.util.concurrent 包下。
- 例如,應用程序的主線程希望在負責啓動框架服務的線程已經啓動所有框架服務之後執行。
2. CountDownLatch 的原理
- CountDownLatch 是通過一個計數器來實現的,計數器的初始值爲線程的數量。
- 每當一個線程完成了自己的任務後,計數器的值就會減 1。
- 當計數器值到達 0 時,表示所有的線程已經完成了任務,然後在閉鎖上等待的線程就可以恢復執行任務。
CountDownLatch 原理
CountDownLatch 的僞代碼
//Main thread start
//Create CountDownLatch for N threads
//Create and start N threads
//Main thread wait on latch
//N threads completes there tasks are returns
//Main thread resume execution
構造函數
//Constructs a CountDownLatch initialized with the given count.
public CountDownLatch(int count) {...}
- 這個計數(count)本質上是閉鎖需要等待的線程數。
- 此值只能設置一次,並且 CountDownLatch 不提供任何其他機制來重置此計數。
- 第一次與 CountDownLatch 的交互是與等待其他線程的主線程進行的。
- 此主線程必須在啓動其他線程後立即調用
CountDownLatch.await()
方法,這樣主線程的操作就會在這個方法上阻塞,直到其他線程完成各自的任務爲止。
- 此主線程必須在啓動其他線程後立即調用
- 其他 N 個線程必須引用閉鎖對象,因爲它們如果完成了任務需要通知 CountDownLatch 對象。
- 此通知是通過
CountDownLatch.countDown()
方法完成的,每次調用計數減少 1。 - 當所有 N 個線程都調用了這個方法時,計數將達到 0,主線程可以在
await()
方法之後繼續執行。
- 此通知是通過
- CountDownLatch 和 ReentrantLock 一樣,內部使用 Sync 繼承 AQS。
構造函數將計數值(count)傳遞給 Sync,並且設置了 state。
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
......
Sync(int count) {
setState(count);
}
阻塞線程
await 方法
- 直接調用了 AQS 的
acquireSharedInterruptibly()
。- 首先判斷是否被中斷,中斷就拋出異常。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
......
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
tryAcquireShared 方法
- 首先嚐試獲取共享鎖,實現方式和獨佔鎖類似,由 CountDownLatch 實現判斷邏輯。
- state 狀態變量,state 的值代表着待達到條件的線程數,比如初始化爲 5,表示待達到條件的線程數爲 5,每次調用
countDown()
函數都會減 1。
- state 狀態變量,state 的值代表着待達到條件的線程數,比如初始化爲 5,表示待達到條件的線程數爲 5,每次調用
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
- 返回 1 代表獲取成功,返回 -1 代表獲取失敗。如果獲取失敗,需要調用
doAcquireSharedInterruptibly()
。
釋放操作
- countDown 操作實際就是釋放鎖的操作,每調用一次,計數值減少 1。
public void countDown() {
sync.releaseShared(1);
}
同樣是首先嚐試釋放鎖,具體實現在 CountDownLatch 中。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
......
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;
}
}
- 死循環加上 CAS 的方式保證 state 的減 1 操作,當計數值等於 0,代表所有子線程都執行完畢,被
await()
阻塞的線程接着調用doReleaseShared()
喚醒。
限定時間的 await
- CountDownLatch 的 await 方法還有個限定阻塞時間的版本。
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
最後調用 doAcquireSharedNanos()
方法,不同之處只是加了時間的處理。
3. CountDownLatch 的使用場景
實現最大的並行性
- 想同時啓動多個線程,實現最大程度的並行性。
- 例如,想測試一個單例類。如果創建一個初始計數器爲 1 的 CountDownLatch,並讓其他所有線程都在這個鎖上等待,只需要調用一次
countDown()
方法就可以讓其他所有等待的線程同時恢復執行。
- 例如,想測試一個單例類。如果創建一個初始計數器爲 1 的 CountDownLatch,並讓其他所有線程都在這個鎖上等待,只需要調用一次
開始執行前等待 N 個線程完成各自任務
- 例如應用程序啓動類要確保在處理用戶請求前,所有 N 個外部系統都已經啓動和運行了。
死鎖檢測
- 用 N 個線程去訪問共享資源,在每個測試階段線程數量不同,並嘗試產生死鎖。
4. CountDownLatch 的使用案例
- 模擬一個應用程序啓動類,開始就啓動 N 個線程,去檢查 N 個外部服務是否正常並通知閉鎖。
- 啓動類一直在閉鎖上等待,一旦驗證和檢查了所有外部服務,就恢復啓動類執行。
BaseHealthChecker
- 實現 Runnable 接口,負責所有特定的外部服務健康檢查的基類。
import java.util.concurrent.CountDownLatch;
public abstract class BaseHealthChecker implements Runnable {
private CountDownLatch _latch;
private String _serviceName;
private boolean _serviceUp;
public BaseHealthChecker(String serviceName, CountDownLatch latch)
{
super();
this._latch = latch;
this._serviceName = serviceName;
this._serviceUp = false;
}
@Override
public void run() {
try {
verifyService();
_serviceUp = true;
} catch (Throwable t) {
t.printStackTrace(System.err);
_serviceUp = false;
} finally {
if(_latch != null) {
_latch.countDown();
}
}
}
public String getServiceName() {
return _serviceName;
}
public boolean isServiceUp() {
return _serviceUp;
}
public abstract void verifyService();
}
- 以下三個類都繼承自 BaseHealthChecker,引用 CountDownLatch 實例,除了服務名和休眠時間不同外,都實現各自的
verifyService()
方法。
NetworkHealthChecker
import java.util.concurrent.CountDownLatch;
public class NetworkHealthChecker extends BaseHealthChecker
{
public NetworkHealthChecker (CountDownLatch latch)
{
super("Network Service", latch);
}
@Override
public void verifyService()
{
System.out.println("Checking " + this.getServiceName());
try
{
Thread.sleep(7000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(this.getServiceName() + " is UP");
}
}
DatabaseHealthChecker
import java.util.concurrent.CountDownLatch;
public class DatabaseHealthChecker extends BaseHealthChecker
{
public DatabaseHealthChecker (CountDownLatch latch)
{
super("Database Service", latch);
}
@Override
public void verifyService()
{
System.out.println("Checking " + this.getServiceName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.getServiceName() + " is UP");
}
}
CacheHealthChecker
import java.util.concurrent.CountDownLatch;
public class CacheHealthChecker extends BaseHealthChecker
{
public CacheHealthChecker (CountDownLatch latch)
{
super("Cache Service", latch);
}
@Override
public void verifyService()
{
System.out.println("Checking " + this.getServiceName());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.getServiceName() + " is UP");
}
}
ApplicationStartupUtil
- 一個主啓動類,負責初始化閉鎖,然後等待所有服務都被檢查完成,再恢復執行。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class ApplicationStartupUtil
{
private static List<BaseHealthChecker> _services;
private static CountDownLatch _latch;
private ApplicationStartupUtil()
{
}
private final static ApplicationStartupUtil INSTANCE = new ApplicationStartupUtil();
public static ApplicationStartupUtil getInstance()
{
return INSTANCE;
}
public static boolean checkExternalServices() throws Exception
{
_latch = new CountDownLatch(3);
_services = new ArrayList<BaseHealthChecker>();
_services.add(new NetworkHealthChecker(_latch));
_services.add(new CacheHealthChecker(_latch));
_services.add(new DatabaseHealthChecker(_latch));
Executor executor = Executors.newFixedThreadPool(_services.size());
for(final BaseHealthChecker v : _services)
{
executor.execute(v);
}
_latch.await();
for(final BaseHealthChecker v : _services)
{
if( ! v.isServiceUp())
{
return false;
}
}
return true;
}
}
測試代碼
public class Main {
public static void main(String[] args)
{
boolean result = false;
try {
result = ApplicationStartupUtil.checkExternalServices();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("External services validation completed !! Result was :: "+ result);
}
}
/** --- print ---
Checking Network Service
Checking Cache Service
Checking Database Service
Database Service is UP
Cache Service is UP
Network Service is UP
External services validation completed !! Result was :: true
**/
5. 總結
- CountDownLatch 是一次性的,計數器的值只能在構造方法中初始化一次,之後沒有任何機制再次對其設置值,當 CountDownLatch 使用完畢後,不能再次被使用。
- Thread 的
join()
方法可以實現相同的功能,但是當使用了線程池時,則join()
方法便無法實現,CountDownLatch 依然可以實現功能。
package concurrent;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.*;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.*;
public class CountDownLatchDemo {
private final static CountDownLatch cdl=new CountDownLatch(3);
private final static Vector v=new Vector();
private final static ThreadPoolExecutor threadPool= new ThreadPoolExecutor(10, 15, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());//使用線程池
private static class WriteThread extends Thread{
private final String writeThreadName;
private final int stopTime;
private final String str;
public WriteThread(String name,int time,String str)
{
this.writeThreadName=name;
this.stopTime=time;
this.str=str;
}
public void run()
{
System.out.println(writeThreadName+"開始寫入工作");
try
{
Thread.sleep(stopTime);
}
catch(InterruptedException e)
{
e.printStackTrace();
}
cdl.countDown();
v.add(str);
System.out.println(writeThreadName+"寫入內容爲:"+str+"。寫入工作結束!");
}
}
private static class ReadThread extends Thread{
public void run()
{
System.out.println("讀操作之前必須先進行寫操作");
try
{
cdl.await();//該線程進行等待,直到countDown減到0,然後逐個甦醒過來。
//Thread.sleep(3000);
}
catch(InterruptedException e)
{
e.printStackTrace();
}
for(int i=0;i<v.size();i++)
{
System.out.println("讀取第"+(i+1)+"條記錄內容爲:"+v.get(i));
}
System.out.println("讀操作結束!");
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
Thread read=new ReadThread();
threadPool.execute(read);
String[] str= {"多線程知識點","多線程CountDownLatch的知識點","多線程中控制順序可以使用CountDownLatch"};
for(int i=0;i<3;i++)
{
Thread t1= new WriteThread("writeThread"+(i+1),1000*(i+1),str[i]);
threadPool.execute(t1);
}
//new WriteThread("writeThread1",1000,"多線程知識點").start();
//new WriteThread("writeThread2",2000,"多線程CountDownLatch的知識點").start();
//new WriteThread("writeThread3",3000,"多線程中控制順序可以使用CountDownLatch").start();
}
}
CountDownLatch 類主要使用的場景有明顯的順序要求。
- 比如只有等跑完步才能計算排名,只有等所有記錄都寫入才能進行統計工作等等,因此 CountDownLatch 完善的是某種邏輯上的功能,使得線程按照正確的邏輯進行。
- CountDownLatch類主要是用來實現線程的按順序執行。主要通過count計數器來實現功能,CountDownLatch(int count)構造函數用於初始化同步計時器,只有當同步計時器爲0,主線程纔會向下執行。而實現按順序執行的兩個主要方法便是await()和countDown(),其中await()使暫時不想讓它執行的線程加入隊列進入阻塞狀態,而countDown()則是每當執行完一個可執行線程便會減1,直到count爲0,那麼阻塞隊列的線程便會被喚醒。
- Thread對象的join方法可以實現相同的功能,但是特別地,當使用了線程池時,則join()方法便無法實現。但CountDownLatch依然可以實現功能。
- CountDownLatch類主要使用的場景有明顯的順序要求:比如只有等跑完步才能計算排名,只有等所有記錄都寫入才能進行統計工作等等,因此CountDownLatch完善的是某種邏輯上的功能,使得線程按照正確的邏輯進行。