CountDownLatch와 CyclicBarrier는 모두 멀티스레드 환경에서 스레드 간 작업을 조율하기 위해 사용되는 대표적인 동기화 도구로, java.util.concurrent 패키지에 포함되어 있습니다.
Java 공식 문서(JavaDoc)에서는 두 클래스를 각각 다음과 같이 정의하고 있습니다.
- CountDownLatch — 다른 스레드에서 수행 중인 일련의 작업이 완료될 때까지 하나 이상의 스레드가 대기할 수 있도록 지원하는 동기화 보조 클래스입니다.
- CyclicBarrier — 여러 스레드가 서로 공통된 장벽(barrier) 지점에 도달할 때까지 상호 대기할 수 있도록 지원하는 동기화 보조 클래스입니다.
CountDownLatch와 CyclicBarrier의 주요 차이점
| 번호 | 구분 | CyclicBarrier | CountDownLatch |
|---|---|---|---|
| 1 | 기본 개념 | 여러 스레드가 공통된 장벽 지점에 도달할 때까지 서로를 기다리게 하는 동기화 도구입니다. | 다른 스레드에서 수행 중인 작업이 완료될 때까지 하나 이상의 스레드가 대기하게 하는 동기화 도구입니다. |
| 2 | Runnable 지원 | 생성자에서 Runnable(장벽 통과 시 실행될 액션)을 지정할 수 있습니다. | Runnable을 받는 생성자는 제공되지 않습니다. |
| 3 | 관리 대상 | 대기하는 스레드 수를 카운트합니다. | 완료되어야 할 작업(task) 수를 카운트합니다. |
| 4 | 카운트 진행 방식 | 스레드가 실제로 장벽에 도달해야만 진행되므로 외부에서 카운트를 임의로 조작할 수 없습니다. | countDown() 메서드 호출을 통해 카운트를 감소(진행)시킬 수 있습니다. |
| 5 | 예외 처리 | 대기 중인 한 스레드가 인터럽트되면, 나머지 대기 스레드들도 모두 BrokenBarrierException이 발생합니다. | 현재 스레드에만 InterruptedException이 발생하며, 다른 스레드에는 영향을 주지 않습니다. |
| 6 | 재사용 가능 여부 | 장벽이 해제된 후 자동으로 초기화되므로 재사용이 가능합니다(reset() 지원). | 카운트가 0에 도달하면 더 이상 사용할 수 없는 1회성 도구입니다. |
CyclicBarrier 사용 예제
public class Main {
public static void main(String args[]) throws InterruptedException {
ExecutorService executors = Executors.newFixedThreadPool(4);
CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
executors.submit(new Service1(cyclicBarrier));
executors.submit(new Service1(cyclicBarrier));
executors.submit(new Service2(cyclicBarrier));
executors.submit(new Service2(cyclicBarrier));
executors.submit(new Service2(cyclicBarrier));
Thread.sleep(3000);
System.out.println("Done");
}
}
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class Service1 implements Runnable {
CyclicBarrier cyclicBarrier;
public Service1(CyclicBarrier cyclicBarrier) {
super();
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
System.out.println("Services1 : " + cyclicBarrier.getNumberWaiting());
while (true) {
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class Service2 implements Runnable {
CyclicBarrier cyclicBarrier;
public Service2(CyclicBarrier cyclicBarrier) {
super();
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
System.out.println("Services2 : " + cyclicBarrier.getNumberWaiting());
while (true) {
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
CountDownLatch 사용 예제
public class Main {
public static void main(String args[]) throws InterruptedException {
ExecutorService executors = Executors.newFixedThreadPool(4);
CountDownLatch latch = new CountDownLatch(2);
executors.submit(new Service1(latch));
executors.submit(new Service2(latch));
latch.await();
System.out.println("Done");
}
}
import java.util.concurrent.CountDownLatch;
public class Service1 implements Runnable {
CountDownLatch latch;
public Service1(CountDownLatch latch) {
super();
this.latch = latch;
}
@Override
public void run() {
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown();
System.out.println("Services1 : " + latch.getCount());
}
}
import java.util.concurrent.CountDownLatch;
public class Service2 implements Runnable {
CountDownLatch latch;
public Service2(CountDownLatch latch) {
super();
this.latch = latch;
}
@Override
public void run() {
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown();
System.out.println("Services2 : " + latch.getCount());
}
}
정리
CountDownLatch는 특정 작업이나 이벤트가 정해진 횟수만큼 완료될 때까지 대기 스레드를 묶어두는 1회성 래치(latch)이고, CyclicBarrier는 정해진 수의 스레드가 특정 지점에서 서로 만날 때까지 대기한 뒤 함께 진행하며, 사용 후에도 재사용할 수 있는 순환 장벽입니다. 작업 완료 시점까지 기다려야 한다면 CountDownLatch가, 여러 스레드가 특정 시점에 동기화된 후 반복적으로 협업해야 한다면 CyclicBarrier가 더 적합한 선택입니다.