Java의 CountDownLatch 가이드

1. 소개

이 기사에서는 CountDownLatch 클래스에 대한 가이드를 제공 하고 몇 가지 실제 예제에서 어떻게 사용할 수 있는지 보여줍니다.

기본적으로 CountDownLatch 를 사용하면 다른 스레드가 주어진 작업을 완료 할 때까지 스레드가 차단되도록 할 수 있습니다.

2. 동시 프로그래밍에서의 사용

간단히 말해서 CountDownLatch 에는 카운터 필드가 있으며 필요한만큼 줄일 수 있습니다. 그런 다음 0까지 카운트 다운 될 때까지 호출 스레드를 차단하는 데 사용할 수 있습니다.

병렬 처리를 수행하는 경우 작업하려는 스레드 수와 동일한 카운터 값으로 CountDownLatch 를 인스턴스화 할 수 있습니다. 그런 다음 각 스레드가 완료된 후 countdown () 을 호출하여 await () 를 호출하는 종속 스레드 가 작업자 스레드가 완료 될 때까지 차단되도록 할 수 있습니다.

3. 스레드 풀이 완료 될 때까지 대기

Worker 를 만들고 완료되면 CountDownLatch 필드를 사용하여 신호를 보내이 패턴을 시도해 보겠습니다 .

public class Worker implements Runnable { private List outputScraper; private CountDownLatch countDownLatch; public Worker(List outputScraper, CountDownLatch countDownLatch) { this.outputScraper = outputScraper; this.countDownLatch = countDownLatch; } @Override public void run() { doSomeWork(); outputScraper.add("Counted down"); countDownLatch.countDown(); } }

그런 다음 Worker 인스턴스가 완료 될 때까지 기다리는 CountDownLatch 를 얻을 수 있음을 증명하기 위해 테스트를 생성 해 보겠습니다 .

@Test public void whenParallelProcessing_thenMainThreadWillBlockUntilCompletion() throws InterruptedException { List outputScraper = Collections.synchronizedList(new ArrayList()); CountDownLatch countDownLatch = new CountDownLatch(5); List workers = Stream .generate(() -> new Thread(new Worker(outputScraper, countDownLatch))) .limit(5) .collect(toList()); workers.forEach(Thread::start); countDownLatch.await(); outputScraper.add("Latch released"); assertThat(outputScraper) .containsExactly( "Counted down", "Counted down", "Counted down", "Counted down", "Counted down", "Latch released" ); }

당연히 "래치 해제"는 CountDownLatch 해제 에 따라 항상 마지막 출력이됩니다 .

await ()를 호출하지 않으면 스레드 실행 순서를 보장 할 수 없으므로 테스트가 무작위로 실패합니다.

4. 시작을 기다리는 스레드 풀

이전 예제를 사용했지만 이번에 5 개가 아닌 수천 개의 스레드를 시작했다면, 이후의 스레드에서 start () 를 호출하기 전에 많은 이전 스레드가 처리를 완료했을 가능성이 높습니다 . 이로 인해 모든 스레드를 병렬로 실행할 수 없기 때문에 동시성 문제를 시도하고 재현하기가 어려울 수 있습니다.

이 문제를 해결하기 위해 CountdownLatch 가 이전 예제와 다르게 작동 하도록하겠습니다 . 일부 자식 스레드가 완료 될 때까지 부모 스레드를 차단하는 대신 다른 모든 스레드가 시작될 때까지 각 자식 스레드를 차단할 수 있습니다.

처리 전에 차단되도록 run () 메서드를 수정 해 보겠습니다 .

public class WaitingWorker implements Runnable { private List outputScraper; private CountDownLatch readyThreadCounter; private CountDownLatch callingThreadBlocker; private CountDownLatch completedThreadCounter; public WaitingWorker( List outputScraper, CountDownLatch readyThreadCounter, CountDownLatch callingThreadBlocker, CountDownLatch completedThreadCounter) { this.outputScraper = outputScraper; this.readyThreadCounter = readyThreadCounter; this.callingThreadBlocker = callingThreadBlocker; this.completedThreadCounter = completedThreadCounter; } @Override public void run() { readyThreadCounter.countDown(); try { callingThreadBlocker.await(); doSomeWork(); outputScraper.add("Counted down"); } catch (InterruptedException e) { e.printStackTrace(); } finally { completedThreadCounter.countDown(); } } }

모든 때까지 차단 그래서 지금, 우리의 테스트를 수정할 수 있습니다 노동자 시작의 차단을 해제 노동자를, 다음 될 때까지 블록 근로자 완료 :

@Test public void whenDoingLotsOfThreadsInParallel_thenStartThemAtTheSameTime() throws InterruptedException { List outputScraper = Collections.synchronizedList(new ArrayList()); CountDownLatch readyThreadCounter = new CountDownLatch(5); CountDownLatch callingThreadBlocker = new CountDownLatch(1); CountDownLatch completedThreadCounter = new CountDownLatch(5); List workers = Stream .generate(() -> new Thread(new WaitingWorker( outputScraper, readyThreadCounter, callingThreadBlocker, completedThreadCounter))) .limit(5) .collect(toList()); workers.forEach(Thread::start); readyThreadCounter.await(); outputScraper.add("Workers ready"); callingThreadBlocker.countDown(); completedThreadCounter.await(); outputScraper.add("Workers complete"); assertThat(outputScraper) .containsExactly( "Workers ready", "Counted down", "Counted down", "Counted down", "Counted down", "Counted down", "Workers complete" ); }

이 패턴은 수천 개의 스레드가 일부 논리를 병렬로 시도하고 수행하도록 강제하는 데 사용할 수 있으므로 동시성 버그를 재현하는 데 매우 유용합니다.

5. 카운트 다운 래치 조기 종료

때로는 CountDownLatch 를 카운트 다운하기 전에 Workers 가 오류로 종료 되는 상황이 발생할 수 있습니다 . 이로 인해 0에 도달하지 않고 await ()가 종료되지 않을 수 있습니다 .

@Override public void run() { if (true) { throw new RuntimeException("Oh dear, I'm a BrokenWorker"); } countDownLatch.countDown(); outputScraper.add("Counted down"); }

await () 가 어떻게 영원히 차단 되는지 보여주기 위해 BrokenWorker 를 사용하도록 이전 테스트를 수정 해 보겠습니다 .

@Test public void whenFailingToParallelProcess_thenMainThreadShouldGetNotGetStuck() throws InterruptedException { List outputScraper = Collections.synchronizedList(new ArrayList()); CountDownLatch countDownLatch = new CountDownLatch(5); List workers = Stream .generate(() -> new Thread(new BrokenWorker(outputScraper, countDownLatch))) .limit(5) .collect(toList()); workers.forEach(Thread::start); countDownLatch.await(); }

분명히 이것은 우리가 원하는 동작이 아닙니다. 무한히 차단하는 것보다 응용 프로그램이 계속되는 것이 훨씬 낫습니다.

이 문제를 해결하기 위해 await () 호출에 시간 초과 인수를 추가해 보겠습니다 .

boolean completed = countDownLatch.await(3L, TimeUnit.SECONDS); assertThat(completed).isFalse();

보시다시피 테스트는 결국 시간 초과되고 await ()false 를 반환 합니다.

6. 결론

이 빠른 가이드에서는 다른 스레드가 일부 처리를 완료 할 때까지 스레드를 차단하기 위해 CountDownLatch 를 사용하는 방법을 보여주었습니다 .

또한 스레드가 병렬로 실행되도록하여 동시성 문제를 디버그하는 데 사용할 수있는 방법도 보여주었습니다.

이러한 예제의 구현은 GitHub에서 찾을 수 있습니다. 이것은 Maven 기반 프로젝트이므로 그대로 실행하기 쉽습니다.