Java 동시성 프로그래밍에서 "특정 작업들이 모두 끝날 때까지 기다려야 하는" 상황,
CountDownLatch하나면 깔끔하게 해결됩니다.
1. CountDownLatch란?
CountDownLatch는 Java 5(java.util.concurrent)에서 도입된 동기화 보조 클래스입니다.
내부에 카운터(count) 를 가지고 있으며, 이 카운터가 0이 될 때까지 하나 이상의 스레드를 대기시키는 역할을 합니다.
- 패키지:
java.util.concurrent.CountDownLatch - 핵심 개념: 카운터가 0이 되는 순간 대기 중인 스레드가 일제히 진행을 재개

2. 핵심 메서드
| 메서드 | 설명 |
CountDownLatch(int count) |
카운터 초기값을 설정하여 생성 |
await() |
카운터가 0이 될 때까지 현재 스레드를 블로킹 |
await(long timeout, TimeUnit unit) |
지정한 시간만큼만 대기 (타임아웃 지원) |
countDown() |
카운터를 1 감소. 0이 되면 대기 중인 스레드 해제 |
getCount() |
현재 카운터 값 반환 |
3. 기본 예제 코드
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
int workerCount = 3;
CountDownLatch latch = new CountDownLatch(workerCount);
ExecutorService executor = Executors.newFixedThreadPool(workerCount);
for (int i = 1; i <= workerCount; i++) {
final int workerId = i;
executor.submit(() -> {
try {
System.out.println("워커 " + workerId + " 작업 시작");
Thread.sleep((long) (Math.random() * 2000)); // 작업 시뮬레이션
System.out.println("워커 " + workerId + " 작업 완료 → countDown()");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown(); // ★ 반드시 finally에서 호출!
}
});
}
System.out.println("메인 스레드: 모든 워커 대기 중...");
latch.await(); // 카운터가 0이 될 때까지 블로킹
System.out.println("메인 스레드: 모든 워커 완료! 후속 작업 실행");
executor.shutdown();
}
}
출력 예시 (순서는 달라질 수 있음):
메인 스레드: 모든 워커 대기 중...
워커 2 작업 시작
워커 1 작업 시작
워커 3 작업 시작
워커 2 작업 완료 → countDown()
워커 1 작업 완료 → countDown()
워커 3 작업 완료 → countDown()
메인 스레드: 모든 워커 완료! 후속 작업 실행
4. 동작 타임라인
위 코드의 실행 흐름을 시간 축으로 나타내면 다음과 같습니다.

흐름 요약:
- 메인 스레드가
new CountDownLatch(3)으로 카운터를 3으로 초기화 - 메인 스레드는
await()을 호출하여 대기 - 각 워커 스레드는 작업 완료 후
countDown()호출 → 카운터 감소 - 카운터가 0이 되는 순간 메인 스레드가 깨어나 후속 작업 진행
5. 타임아웃 사용 예제
무한정 대기하면 안 되는 경우엔 await(timeout, unit)을 사용합니다.
import java.util.concurrent.TimeUnit;
boolean completed = latch.await(5, TimeUnit.SECONDS);
if (completed) {
System.out.println("모든 작업 완료!");
} else {
System.out.println("타임아웃! 일부 작업이 완료되지 않았습니다.");
// 타임아웃 후 처리 로직
}
6. 실전 활용 패턴
패턴 1 — 시작 신호 (Start Gate)
여러 스레드를 동시에 시작시키고 싶을 때 사용합니다. (성능 테스트에 유용)
CountDownLatch startGate = new CountDownLatch(1); // 시작 신호용
CountDownLatch endGate = new CountDownLatch(N); // 완료 대기용
for (int i = 0; i < N; i++) {
new Thread(() -> {
try {
startGate.await(); // 신호 대기
// ... 실제 작업 수행 ...
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
endGate.countDown();
}
}).start();
}
startGate.countDown(); // 모든 스레드 동시 출발!
endGate.await(); // 전부 끝날 때까지 대기
패턴 2 — 초기화 완료 대기
서비스 시작 전에 필요한 초기화 작업들이 모두 완료될 때까지 기다립니다.
CountDownLatch initLatch = new CountDownLatch(3);
// DB 연결, 캐시 로딩, 설정 로딩을 병렬로 실행
executor.submit(() -> { connectDB(); initLatch.countDown(); });
executor.submit(() -> { loadCache(); initLatch.countDown(); });
executor.submit(() -> { loadConfig(); initLatch.countDown(); });
initLatch.await(); // 초기화 완료까지 대기
startServer(); // 서버 시작
7. CountDownLatch vs CompletableFuture.join()
둘 다 "다른 작업이 끝날 때까지 현재 스레드를 블로킹"한다는 점에서 비슷하게 느껴지지만 목적이 다릅니다.
| 비교 항목 | CountDownLatch | CompletableFuture.join() |
| 기다리는 대상 | 이벤트(카운트 신호) | 작업의 결과값 |
| 결과값 반환 | ❌ 없음 | ✅ 있음 |
| 여러 작업 처리 | 카운터 하나로 N개 통합 | allOf(...).join()으로 묶음 |
| 카운트 주체 | 누구든 countDown() 호출 가능 |
Future 객체 단위로 명확히 정의 |
| 예외 처리 | InterruptedException (checked) |
CompletionException (unchecked) |
| 주요 용도 | 완료 신호 동기화, 테스트, 시작 게이트 | 비동기 결과 수집 및 체이닝 |
선택 기준: "작업 결과를 모아서 뭔가를 해야 한다" →
CompletableFuture
"N개의 완료 신호만 확인하면 된다" →CountDownLatch
8. CountDownLatch vs CyclicBarrier
비슷해 보이지만 용도가 다릅니다.
| 비교 항목 | CountDownLatch | CyclicBarrier |
| 재사용 | ❌ 불가 (1회성) | ✅ 가능 (reset 후 재사용) |
| 대기 주체 | 다른 스레드가 카운트다운 | 서로를 기다림 (상호 대기) |
| 카운트 주체 | 누구든 countDown() 호출 가능 |
참여 스레드만 가능 |
| 용도 | 완료 이벤트 대기 | 집결점(rendezvous) 동기화 |
| 스레드 수 | 대기자 ≠ 카운트 주체 | 대기자 = 카운트 주체 |
선택 기준: "한 스레드가 여러 작업의 완료를 기다린다" →
CountDownLatch
"여러 스레드가 서로를 기다리며 단계를 함께 진행한다" →CyclicBarrier
9. 주의사항 & Best Practice
반드시 finally 블록에서 countDown() 호출
예외가 발생해도 카운트다운이 누락되지 않도록 합니다.
try {
doWork();
} finally {
latch.countDown(); // 예외 발생 여부와 무관하게 반드시 실행
}
재사용이 필요하다면 CyclicBarrier 사용
CountDownLatch는 카운터가 0이 된 이후 리셋 불가입니다.
// ❌ 잘못된 사용 - 재사용 시도
latch.await();
// latch를 다시 사용하려 하면 count가 이미 0이라 즉시 통과해버림!
// ✅ 재사용이 필요하면 새로 생성하거나 CyclicBarrier 사용
타임아웃을 적극 활용
운영 환경에서는 특정 작업이 무한 대기에 빠질 수 있으므로, 반드시 타임아웃을 설정합니다.
10. 정리
CountDownLatch(N)
│
├─ await() → 카운터가 0이 될 때까지 블로킹
├─ countDown() → 카운터 1 감소 (0이 되면 대기 스레드 해제)
└─ getCount() → 현재 카운터 값 확인
핵심 특징:
- 1회성 (재사용 불가)
- 카운트다운 주체와 대기 주체가 다를 수 있음
- 여러 스레드가 동시에 await() 가능
- 카운터가 0이 되는 순간 모든 대기 스레드 해제
참고 자료
- Java 공식 문서 — CountDownLatch
- Effective Java 3판, 아이템 81 — wait과 notify보다는 동시성 유틸리티를 애용하라
'Language > Java' 카테고리의 다른 글
| [Java] JVM 클래스 로딩 Deep Dive 2편 — InstanceKlass, Class 객체, Initialization (0) | 2026.05.17 |
|---|---|
| [Java] JVM 클래스 로딩 Deep Dive 1편 — Loading & Linking (0) | 2026.05.17 |
| [Java] Java Concurrent 패키지 - Concurrent Collections 완벽 정리 (0) | 2026.05.17 |
| [JAVA] Java Concurrent 패키지 - Atomic 자료형 완전 정리 (1) | 2026.05.14 |
| [Java] Java 비동기 결과 처리의 진화: Callable → Future → CompletableFuture (0) | 2026.05.14 |