본문으로 건너뛰기
김신건의 로그

[Java] CountDownLatch

· 수정 · 📖 약 2분 · 672자/단어 #java #concurrent #synchronizer #latch #one-shot
CountDownLatch, java.util.concurrent.CountDownLatch, 카운트다운 래치, countdown latch

정의

java.util.concurrent.CountDownLatch한 스레드 또는 여러 스레드가 다른 스레드들의 작업 완료를 기다리는 일회용 synchronizer.

N 으로 초기화하고, 각 worker 가 작업 후 countDown() 호출. waiter 는 await() 로 count 가 0 이 될 때까지 Blocking. 0 이 되면 다시 1 로 돌릴 수 없음, 일회용.

JDK 1.5 도입. AbstractQueuedSynchronizer (AQS) 를 기반으로 구현된다.

언제 쓰나

  • N 개 작업 완료 대기: main 스레드가 병렬 작업 전체가 끝날 때까지 기다릴 때
  • 시작 신호 (start gate): 여러 스레드를 동시에 출발시킬 때 (부하 테스트 등)
  • 초기화 완료 대기: 서비스 시작 전 의존 컴포넌트가 모두 준비될 때까지 대기
  • 일회성 이벤트: 재사용이 필요 없는 단순 동기화 지점

IMPORTANT

재사용이 필요하면 CyclicBarrier 또는 Phaser 를 사용. CountDownLatch 는 count 가 0 이 된 후 재설정 불가.

시각화: N 개 작업 완료 대기

sequenceDiagram
    participant M as "Main 스레드"
    participant L as "CountDownLatch(3)"
    participant W1 as "Worker 1"
    participant W2 as "Worker 2"
    participant W3 as "Worker 3"

    M->>L: await() - count=3, 블록
    W1->>L: countDown() - count=2
    W2->>L: countDown() - count=1
    W3->>L: countDown() - count=0
    L-->>M: count=0, 깨어남
    Note over M: 모든 작업 완료 후 진행

시각화: 시작 신호 (start gate)

sequenceDiagram
    participant M as "Main 스레드"
    participant G as "CountDownLatch(1)"
    participant T1 as "Thread 1"
    participant T2 as "Thread 2"
    participant T3 as "Thread 3"

    T1->>G: await() - 대기
    T2->>G: await() - 대기
    T3->>G: await() - 대기
    Note over M: 모든 스레드 준비 확인
    M->>G: countDown() - count=0
    G-->>T1: 동시 출발
    G-->>T2: 동시 출발
    G-->>T3: 동시 출발

핵심 메서드

CountDownLatch latch = new CountDownLatch(N);

latch.countDown();                      // count - 1 (0 이하로 내려가지 않음)
latch.await();                          // count == 0 될 때까지 block
boolean done = latch.await(5, TimeUnit.SECONDS);  // timeout 있는 대기
latch.getCount();                       // 현재 count (참고용, 동기화 목적 사용 금지)

가장 흔한 두 패턴

1. main 스레드가 N 개 작업 완료 대기

import java.util.concurrent.*;

int N = 5;
ExecutorService pool = Executors.newFixedThreadPool(N);
CountDownLatch done = new CountDownLatch(N);

for (int i = 0; i < N; i++) {
    final int taskId = i;
    pool.submit(() -> {
        try {
            doWork(taskId);
        } finally {
            done.countDown();   // 반드시 finally: 예외 발생해도 countDown 보장
        }
    });
}

done.await();   // 모든 작업 끝날 때까지 block
pool.shutdown();
System.out.println("all done");

2. 시작 신호 (start gate)

import java.util.concurrent.*;

int N = 10;
CountDownLatch startGate = new CountDownLatch(1);
CountDownLatch endGate = new CountDownLatch(N);

for (int i = 0; i < N; i++) {
    Thread.ofVirtual().start(() -> {
        try {
            startGate.await();   // start 신호 대기
            run();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            endGate.countDown();
        }
    });
}

// 모든 스레드가 준비됐을 때 동시 출발
startGate.countDown();
endGate.await();   // 모든 스레드 완료 대기

부하 테스트 도구 (JMeter, Gatling 의 ramp-up) 와 비슷한 패턴.

내부 구현: AQS 기반

CountDownLatchAbstractQueuedSynchronizer (AQS) 의 공유 모드를 사용한다.

public class CountDownLatch {
    private static final class Sync extends AbstractQueuedSynchronizer {
        Sync(int count) {
            setState(count);   // AQS state = count
        }

        // tryAcquireShared: state == 0 이면 통과 (1), 아니면 대기 (-1)
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

        // tryReleaseShared: CAS 로 state - 1, 0 이 되면 true (대기자 깨움)
        protected boolean tryReleaseShared(int releases) {
            for (;;) {
                int c = getState();
                if (c == 0) return false;
                int nextc = c - 1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }

    private final Sync sync;

    public CountDownLatch(int count) {
        this.sync = new Sync(count);
    }

    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

    public void countDown() {
        sync.releaseShared(1);
    }
}
  • countDown(): CAS 로 state 를 1 감소. 0 이 되면 AQS 대기 큐의 모든 스레드를 깨운다.
  • await(): state 가 0 이 아니면 AQS 대기 큐에 진입해 park.
  • count 가 0 이 된 후 추가 countDown() 은 무시 (state 가 이미 0).

성능과 스레드 안전성

  • countDown(): CAS 연산 하나. 락 없음. 매우 빠름.
  • await(): state 가 0 이면 즉시 반환 (락 없음). 0 이 아니면 LockSupport.park() 로 블록.
  • 여러 스레드가 동시에 countDown() 해도 안전: CAS 루프로 직렬화.
  • 여러 스레드가 동시에 await() 해도 안전: count 가 0 이 되면 모두 동시에 깨어남.

Java 17+ 실전: 서비스 초기화 대기

import java.util.concurrent.*;

// 여러 서비스가 모두 준비될 때까지 대기
class ServiceOrchestrator {
    private final CountDownLatch ready;
    private final List<String> errors = new CopyOnWriteArrayList<>();

    ServiceOrchestrator(int serviceCount) {
        this.ready = new CountDownLatch(serviceCount);
    }

    void registerService(String name, Callable<Void> initializer) {
        Thread.ofVirtual().start(() -> {
            try {
                initializer.call();
                System.out.println(name + " ready");
            } catch (Exception e) {
                errors.add(name + ": " + e.getMessage());
            } finally {
                ready.countDown();   // 성공/실패 모두 countDown
            }
        });
    }

    boolean awaitReady(long timeout, TimeUnit unit) throws InterruptedException {
        boolean allDone = ready.await(timeout, unit);
        if (!errors.isEmpty()) {
            throw new IllegalStateException("Init errors: " + errors);
        }
        return allDone;
    }
}

Java 17+ 실전: 병렬 데이터 수집

import java.util.concurrent.*;
import java.util.concurrent.atomic.*;

// 여러 소스에서 병렬로 데이터 수집 후 집계
record FetchResult(String source, List<String> data) {}

List<FetchResult> parallelFetch(List<String> sources) throws InterruptedException {
    CountDownLatch latch = new CountDownLatch(sources.size());
    List<FetchResult> results = new CopyOnWriteArrayList<>();

    for (String source : sources) {
        Thread.ofVirtual().start(() -> {
            try {
                List<String> data = fetchFrom(source);
                results.add(new FetchResult(source, data));
            } catch (Exception e) {
                results.add(new FetchResult(source, List.of()));
            } finally {
                latch.countDown();
            }
        });
    }

    latch.await(30, TimeUnit.SECONDS);
    return List.copyOf(results);
}

CountDownLatch vs CyclicBarrier vs Phaser

항목CountDownLatchCyclicBarrierPhaser
사용 횟수일회용재사용 가능재사용 가능
누가 기다리는가한 명 또는 여러 명모두가 서로 기다림동적 참여자
도착 콜백없음barrier actiononAdvance()
참여자 수고정고정동적 변경 가능
reset불가reset() 가능자동
복잡도단순중간복잡

CountDownLatch 는 비대칭 (1 -> N 또는 N -> 1), CyclicBarrier 는 대칭 (N -> N 만남).

함정

1. 0 이후 다시 못 쓴다

CountDownLatch latch = new CountDownLatch(1);
latch.countDown();   // count = 0
// latch 재사용 불가. 새 CountDownLatch 생성 필요
// 재사용이 필요하면 CyclicBarrier 또는 Phaser 사용

2. countDown 빠뜨리면 영원히 block

// 위험: 예외 발생 시 countDown 미호출
pool.submit(() -> {
    doWork();           // 예외 발생 가능
    done.countDown();   // 예외 발생 시 실행 안 됨 → main 영원히 대기
});

// 올바름: finally 에서 countDown
pool.submit(() -> {
    try {
        doWork();
    } finally {
        done.countDown();   // 항상 실행
    }
});

3. timeout 없는 await 는 위험

// 위험: worker 가 영원히 끝나지 않으면 main 도 영원히 대기
latch.await();

// 올바름: timeout 지정
boolean completed = latch.await(30, TimeUnit.SECONDS);
if (!completed) {
    // 타임아웃 처리
    throw new TimeoutException("작업이 30초 내에 완료되지 않음");
}

4. getCount() 로 동기화 판단 금지

// 위험: getCount() 는 스냅샷, 확인 후 상태가 바뀔 수 있음
if (latch.getCount() == 0) {
    // 이 시점에 count 가 다시 0 이 아닐 수 있음 (실제로는 불가하지만 패턴 자체가 위험)
    proceed();
}

// 올바름: await() 사용
latch.await();
proceed();

관련 위키

이 글의 용어 (7개)
[Java] BlockingQueuejava
정의 는 요소를 가져올 때 비어 있으면 대기, 넣을 때 가득 차 있으면 대기 하는 thread-safe 큐 인터페이스. 생산자-소비자 (producer-consumer) 패턴의 …
[Java] Collectionjava
정의 는 그룹으로 묶인 객체들을 표현하는 최상위 인터페이스. JCF (Java Collections Framework) 의 입구이자, / / / 모두 이를 확장한다. 자체는 직접…
[Java] CyclicBarrierjava
정의 는 N 개의 스레드가 모두 도착할 때까지 서로 기다리는 synchronizer. 모두 도착하면 동시에 출발, 다시 사용 가능 (cyclic). 시각화 핵심 메서드 가장 흔한…
[Java] Phaserjava
정의 는 의 확장. 참여자 수가 동적으로 변할 수 있는 단계별 동기화. JDK 1.7 도입. 시각화 핵심 메서드 동적 참여 예 worker 가 도중에 추가/종료될 수 있는 경우 …
[Java] ReentrantLockjava
정의 는 키워드와 같은 상호 배제 (mutual exclusion) 를 제공하는 클래스 기반 락. JSR-166 (Java 5) 에서 추가됐다. "재진입 (reentrant)" …
[Java] Semaphorejava
정의 는 N 개의 permit (허가) 을 발급/회수 하는 synchronizer. permit 이 있으면 진행, 없으면 또는 즉시 실패. 자원 풀, rate limiting, …
Blocking (블로킹)concurrency
정의 Blocking 은 호출이 결과가 준비될 때까지 호출 스레드를 멈춰두는 실행 방식. 멈춰 있는 동안 그 스레드는 다른 일을 할 수 없다. OS 가 스레드를 sleep 상태로…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기