HEROJOON 블로그(히로블)

Java CountDownLatch를 이용한 Thread 대기 예제 본문

Backend

Java CountDownLatch를 이용한 Thread 대기 예제

herojoon 2024. 6. 19. 16:42
반응형

목표

Java에서 제공하는 CountDownLatch를 이해하고 CountDownLatch를 이용하여 Thread 대기 예제 해보기
 

이해하기

CountDownLatch란?

: CountDownLatch는 Java에서 일련의 스레드 작업이 끝난 후 다음 작업이 진행될 수 있도록 대기 기능을 제공해줍니다.
멀티스레드 환경에서 어떠한 작업들이 수행 된 후 다른 작업이 수행될 수 있도록 하기 위하여 사용됩니다.

  • latch의 영어사전 의미: 자물쇠, 걸쇠, 걸쇠를 걸다.

 

CountDownLatch 설명

  • Java 1.5부터 제공된 기능입니다.
  • java.util.concurrent 패키지에 포함되어 있습니다.
  • 다른 스레드에서 수행 중인 일련의 작업이 완료될 때까지 하나 이상의 스레드를 대기할 수 있도록 기능을 제공합니다.
    [대기 방법1] latch의 count가 모두 감소될 때까지 Thread를 계속 대기 시킬 수도 있고
    ex) 어떤 조건이 부합할 경우 countDownLatch.countDown()으로 count를 감소시켜 countDownLatch.await() 대기를 해제하는 방법
    [대기 방법2] latch의 count가 모두 감소될 때까지 Thread를 대기 시키되 최대 Max 시간만큼만 대기하게 할 수도 있습니다.
    ex) countDownLatch.await(2, TimeUnit.MINUTES); 처럼 대기 시간을 주어 정해진 시간이 끝나면 count가 모두 감소하지 않아도 await() 대기가 해제되는 방법

 

CountDownLatch Class 제공기능

● 생성자 (Construct)

  • CountDownLatch는 선언 시 전달받은 count로 초기화 됩니다.
  • 이 값은 꼭 Thread의 개수 값이 아니어도 되며 작업을 대기 시키기 위한 작업 count나 다음 작업 전에 호출되어야 하는 횟수의 의미로 보시면됩니다.
    ex) CountDownLatch countDownLatch = new CountDownLatch(5);

●  주요 메서드 (Methods)

  • await()
    • CountDownLatch 생성자에 주입한 count가 0이 될 때까지 현재 스레드가 대기하도록 해줍니다.
  • await(long timeout, TimeUnit unit)
    • CountDownLatch 생성자에 주입한 count가 0이 될 때까지 현재 스레드가 대기하도록 해주되, 최대 정해진 시간까지만 대기하고 해당 시간까지 count가 0이 되지 않으면 대기를 해제합니다.
  • countDown()
    • CountDownLatch 생성자에 주입한 count(latch의 개수)를 감소시켜줍니다. count가 0이되면 대기 중이던 모든 스레드를 해제합니다.
  • getCount()
    • 현재 latch의 개수를 반환해줍니다.

●  개발 가이드 (Java 공부 시작하시는 분들은 참고하시면 좋을 것 같아 남겨놓습니다.)
CountDownLatch의 내부 동작 코드 Java Docs를 보시면 예제와 제공 메서드에 대한 설명이 적혀있습니다.
보는 방법은 IntelliJ 기준으로 아래처럼 보시면 됩니다.
CountDownLatch에 [Ctrl] +[마우스 클릭]하여 가이드 문서로 이동합니다.
아래와 같이 이동되며, 상단에 아래처럼 예제가 설명되어 있습니다.
아래에는 Method에 대한 설명도 주석으로 명시되어 있습니다.

 

해보기 요약

아래 예제 3개를 만들어보았습니다.

  • 예제1. 다른 Thread들의 작업이 완료될 때까지 기다렸다가 다음 작업 수행하기
  • 예제2. 다른 Thread들의 작업이 완료될 때까지 최대 Max시간만큼 기다렸다가 다음 작업 수행하기
  • 예제3. 여러 CountDownLatch를 이용해서 "여러 Thread들이 모두 준비가 완료될 때까지 기다렸다가 Thread들 작업 로직 수행하기"

 

해보기

예제1. 다른 Thread들의 작업이 완료될 때까지 기다렸다가 다음 작업 수행하기

<예제1 체크 포인트>

// await()에 시간을 지정하지 않음
countDownLatch.await();

 
 
<예제1 코드>
아래 예제는 [threadCount만큼의 작업 스레드들]이 모두 생성 & 수행된 후 [Finish 로그 출력 작업]이 수행될 수 있도록 시나리오를 구성했습니다.
 
threadCount만큼의 작업 스레드가 모두 완료되었는지 체크하기 위하여,
CountDownLatch 선언시 Thread 개수 만큼 count를 생성자 주입하여 대기 count를 지정합니다.
 
CountDownLatch를 이용하여 [작업 스레드들]이 모두 수행 완료될 때까지 [Finish 로그 출력 작업]이 대기할 수 있도록 [Finish 로그 출력 작업] 앞에 await()를 작성해줍니다.
 
작업 스레드가 수행될 때마다 countDown()을 호출하여 latch의 count를 1씩 감소시켜줍니다.
 
latch의 count가 0이 되면 대기상태가 풀리면서 await() 다음의 작업이 수행됩니다.

import java.util.concurrent.CountDownLatch;

public class Main {

    public static void main(String[] args) throws InterruptedException {
        // 코드 실행
        executeCode();
    }

    /**
     * CountDownLatch를 이용해서 "다른 Thread들의 작업이 완료될 때까지 기다렸다가 다음 작업 수행하기"
     */
    public static void executeCode() throws InterruptedException {  // countDownLatch의 await() 사용 시 InterrupedException을 선언해줘야함.
        int threadCount = 5;  // 생성할 Thread 개수
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);

        // Java에서 가장 처음 실행되는 메소드는 Main 메소드이므로 가장 처름 표시되는 Thread는 Main Thread 임.
        // Main 메소드의 실행도 하나의 Thread 이기 때문.
        System.out.println("#### [Start] Thread Name: %s, Thread Id: %s".formatted(
                Thread.currentThread().getName(), Thread.currentThread().getId()));

        // threadCount 개수 만큼 Thread 생성
        for (int i = 0; i < threadCount; i++) {
            new Thread(new Worker(countDownLatch)).start();
        }

        // countDownLatch의 await()를 실행하여 다른 Thread들의 작업이 모두 끝날 때까지 기다림
        // countDownLatch의 await()가 대기가 해제되는 시점: countDownLatch의 countDown()이 실행되면서 countdownLatch 생성 시 선언했던 숫자가 0이 될 때
        countDownLatch.await();

        // 이 부분에 Thread들의 작업이 완료된 후 원하는 작업 수행하기 (저는 로직 대신 아래 로그 출력함.)
        // 다른 Thread들의 작업이 모두 끝나면 await() 대기가 해제되면서 아래 로그가 출력됨
        System.out.println("#### [Finish] Thread Name: %s, Thread Id: %s".formatted(
                Thread.currentThread().getName(), Thread.currentThread().getId()));
    }

    public static class Worker implements Runnable {

        private CountDownLatch countDownLatch;

        // Thread 동작 시 countDownLatch의 countDown()을 실행시켜주기 위해 생성자 주입
        public Worker(CountDownLatch countDownLatch) {
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            System.out.println("#### [Ongoing] Thread Name: %s, Thread Id: %s".formatted(
                    Thread.currentThread().getName(), Thread.currentThread().getId()));

            // Thread run() 시점에 countDownLatch의 countDown()을 이용하여 countdownLatch 생성 시 선언했던 숫자 감소시킴
            countDownLatch.countDown();
        }
    }
}

/**
[결과]

#### [Start] Thread Name: main, Thread Id: 1
#### [Ongoing] Thread Name: Thread-0, Thread Id: 16
#### [Ongoing] Thread Name: Thread-1, Thread Id: 17
#### [Ongoing] Thread Name: Thread-2, Thread Id: 18
#### [Ongoing] Thread Name: Thread-3, Thread Id: 19
#### [Ongoing] Thread Name: Thread-4, Thread Id: 20
#### [Finish] Thread Name: main, Thread Id: 1
**/

 

<예제1 코드 실행 결과>

 

● 예제2. 다른 Thread들의 작업이 완료될 때까지 최대 Max시간만큼 기다렸다가 다음 작업 수행하기
<예제2 체크 포인트>

// await()에 시간을 지정해줌
countDownLatch.await(3, TimeUnit.SECONDS);

 
 
<예제2 코드>
아래 예제는 [Finish 로그 출력 작업]이 [다른 스레드 작업들]을 Max시간만큼 대기했다가 수행될 수 있도록 시나리오를 구성했습니다.
 
작업 스레드들의 수행 완료를 체크하기 위하여,
CountDownLatch 선언시 Thread 개수 만큼 count를 생성자 주입하여 대기 count를 지정합니다.
 
CountDownLatch를 이용하여 [작업 스레드들]이 모두 수행 완료될 때까지 [Finish 로그 출력 작업]이 대기할 수 있도록 [Finish 로그 출력 작업] 앞에 await()를 작성해줍니다.
하지만 무한 대기가 아닌 정해진 시간만큼 대기할 수 있도록 await(3, TimeUnit.SECONDS)처럼 최대 대기 시간을 입력해줍니다. 예제에서는 3초 대기하도록 입력했습니다.
 
작업 스레드가 수행될 때마다 countDown()을 호출하여 latch의 count를 1씩 감소시켜줍니다.
여기서 위 await(3, TimeUnit.SECONDS)로 지정한 최대 대기 시간 3초가 지났을 경우 latch 대기가 해제되는지 확인을 위하여 작업 스레드 run() 실행 로직에 Thread.sleep(5000)이라는 5초 대기를 걸어줍니다.
 
latch의 count가 0이 되기 전 최대 대기 시간이 지남으로써 latch의 대기상태가 풀리고 await() 다음의 작업이 수행됩니다.

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class Main {

    public static void main(String[] args) throws InterruptedException {
        // 코드 실행
        executeCode();
    }

    /**
     * CountDownLatch를 이용해서 "다른 Thread들의 작업이 완료될 때까지 최대 Max시간만큼 기다렸다가 다음 작업 수행하기"
     */
    public static void executeCode() throws InterruptedException {  // countDownLatch의 await() 사용 시 InterrupedException을 선언해줘야함.
        int threadCount = 5;  // 생성할 Thread 개수
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);

        // Java에서 가장 처음 실행되는 메소드는 Main 메소드이므로 가장 처름 표시되는 Thread는 Main Thread 임.
        // Main 메소드의 실행도 하나의 Thread 이기 때문.
        System.out.println("#### [Start] Thread Name: %s, Thread Id: %s".formatted(
                Thread.currentThread().getName(), Thread.currentThread().getId()));

        // threadCount 개수 만큼 Thread 생성
        for (int i = 0; i < threadCount; i++) {
            new Thread(new Worker(countDownLatch)).start();
        }

        // countDownLatch의 await()를 실행하여 다른 Thread들의 작업이 모두 끝날 때까지 기다림
        // countDownLatch의 await()가 해제되는 시점: countDownLatch의 countDown()이 실행되면서 countdownLatch 생성 시 선언했던 숫자가 0이 될 때
        countDownLatch.await(3, TimeUnit.SECONDS);  // 3초

        // 이 부분에 Thread들의 작업이 완료된 후 원하는 작업 수행하기 (저는 로직 대신 아래 로그 출력함.)
        // 다른 Thread들의 작업이 모두 끝나면 await() 대기가 해제되면서 아래 로그가 출력됨
        System.out.println("#### [Finish] Thread Name: %s, Thread Id: %s".formatted(
                Thread.currentThread().getName(), Thread.currentThread().getId()));
    }

    public static class Worker implements Runnable {

        private CountDownLatch countDownLatch;

        // Thread 동작 시 countDownLatch의 countDown()을 실행시켜주기 위해 생성자 주입
        public Worker(CountDownLatch countDownLatch) {
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            try {
            	// countDownLatch.await()에 3초를 Max 대기시간으로 주었으므로
                // 해당 시간이 넘었을 때 latch가 해제되는지 확인을 위해 5초 동안 기다린 후 로직 실행
                Thread.sleep(5000);  // 5000의 뜻은 5초 대기 (1000 = 1초)

                System.out.println("#### [Ongoing] Thread Name: %s, Thread Id: %s".formatted(
                        Thread.currentThread().getName(), Thread.currentThread().getId()));
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            // Thread run() 시점에 countDownLatch의 countDown()을 이용하여 countdownLatch 생성 시 선언했던 숫자 감소시킴
            countDownLatch.countDown();
        }
    }
}

/**
[결과]

#### [Start] Thread Name: main, Thread Id: 1
#### [Finish] Thread Name: main, Thread Id: 1
#### [Ongoing] Thread Name: Thread-2, Thread Id: 18
#### [Ongoing] Thread Name: Thread-3, Thread Id: 19
#### [Ongoing] Thread Name: Thread-4, Thread Id: 20
#### [Ongoing] Thread Name: Thread-0, Thread Id: 16
#### [Ongoing] Thread Name: Thread-1, Thread Id: 17
**/

 
<예제2 코드 실행 결과>

 
 

예제3. 여러 CountDownLatch를 이용해서 "다른 Thread들의 작업이 완료될 때까지 기다렸다가 다음 작업 수행하기"

<예제3 체크 포인트>

// CountDownLatch를 여러개 사용
CountDownLatch startSignal = new CountDownLatch(threadCount);
CountDownLatch doneSignal = new CountDownLatch(threadCount);

 
 
<예제3 코드>
아래 예제는 다수 CountDownLatch를 사용하여 여러 단계로 대기하며 동작들이 수행되도록 시나리오를 구성했습니다.

  1. threadCount만큼 Thread가 모두 생성 & 준비 완료될 때까지 다음 작업 대기
  2. threadCount만큼 생성된 Thread의 로든 작업이 수행 완료될 때까지 다음 작업 대기
  3. 위 작업들이 모두 완료되면 [Finish 로그 출력 작업] 수행

 
작업 스레드들의 생성 및 수행 완료를 체크하기 위하여,
CountDownLatch 선언시 Thread 개수 만큼 count를 생성자 주입하여 대기 count를 지정합니다.

// 모든 작업 Thread가 작업할 준비가 되었을 때까지 기다려주는 역할
CountDownLatch startSignal = new CountDownLatch(threadCount);

// 모든 작업 Thread가 모두 수행되었는지 체크하는 역할
CountDownLatch doneSignal = new CountDownLatch(threadCount);

 
작업 스레드가 생성(준비)될 때마다 countDown()을 호출하여 startSignal latch의 count를 1씩 감소시켜줍니다.
이때, 모든 작업 스레드가 준비된 후 작업 스레드 로직이 수행되도록 startSignal latch를 await()로 대기시켜줍니다.
 
위 startSignal latch의 대기가 해제되면 다음 작업을 수행해줍니다.
작업 스레드가 수행될 때마다 countDown()을 호출하여  doneSignal latch의 count를 1씩 감소시켜줍니다.
 
위 작업들이 모두 완료되면 doneSignal latch의 대기도 해제되면서 [Finish 로그 출력 작업] 수행됩니다.

import java.util.concurrent.CountDownLatch;

public class Main {
    
    public static void main(String[] args) throws InterruptedException {
        // 코드 실행
        executeCode();
    }

    /**
     * 3. 여러 CountDownLatch를 이용해서 "여러 Thread들이 모두 준비가 완료될 때까지 기다렸다가 Thread들 작업 로직 수행하기"
     */
    public static void executeCode() throws InterruptedException {  // countDownLatch의 await() 사용 시 InterrupedException을 선언해줘야함.
        int threadCount = 5;  // 생성할 Thread 개수
        CountDownLatch startSignal = new CountDownLatch(threadCount);  // 모든 작업 Thread가 작업할 준비가 되었을 때까지 기다려주는 역할 (모든 Thread가 생성되었는지 정확하게 확인하기 위해 threadCount를 초기값으로 넣어줌)
        CountDownLatch doneSignal = new CountDownLatch(threadCount);  // 모든 작업 Thread가 모두 동작되었는지 체크하는 역할

        System.out.println("#### [Start] Thread Name: %s, Thread Id: %s".formatted(
                Thread.currentThread().getName(), Thread.currentThread().getId()));

        for (int i = 0; i < threadCount; ++i) { // create and start threads
            new Thread(new Worker(startSignal, doneSignal)).start();
        }

        doneSignal.await();  // 모든 Thread의 작업이 수행되었다면 await() 대기가 해제되면서 그 다음 로직 실행

        // 예제이기 때문에 별도 로직 대신 로그가 출력되게 함.
        System.out.println("#### [Finish] Thread Name: %s, Thread Id: %s".formatted(
                Thread.currentThread().getName(), Thread.currentThread().getId()));
    }

    public static class Worker implements Runnable {

        private final CountDownLatch startSignal;
        private final CountDownLatch doneSignal;

        /*
            startSignal: Thread가 모두 준비되었는지 체크하기 위해 생성자 주입
            doneSignal: Thread 동작 시 countDownLatch의 countDown()을 실행시켜주기 위해 생성자 주입
         */
        public Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
            this.startSignal = startSignal;
            this.doneSignal = doneSignal;
        }

        @Override
        public void run() {
            System.out.println("#### [Start] Thread Name: %s, Thread Id: %s".formatted(
                    Thread.currentThread().getName(), Thread.currentThread().getId()));

            try {
                startSignal.countDown();  // 모든 Thread가 start()되었는지 체크하며 countDown()해줌.
                startSignal.await();  // 모든 Thread가 start() 되었다면 await() 대기가 해제되면서 run()로직 실행.

                System.out.println("#### [Ongoing] Thread Name: %s, Thread Id: %s".formatted(
                        Thread.currentThread().getName(), Thread.currentThread().getId()));

                doneSignal.countDown();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

/**
[결과]

#### [Start] Thread Name: main, Thread Id: 1
#### [Start] Thread Name: Thread-0, Thread Id: 16
#### [Start] Thread Name: Thread-1, Thread Id: 17
#### [Start] Thread Name: Thread-2, Thread Id: 18
#### [Start] Thread Name: Thread-3, Thread Id: 19
#### [Start] Thread Name: Thread-4, Thread Id: 20
#### [Ongoing] Thread Name: Thread-4, Thread Id: 20
#### [Ongoing] Thread Name: Thread-2, Thread Id: 18
#### [Ongoing] Thread Name: Thread-0, Thread Id: 16
#### [Ongoing] Thread Name: Thread-1, Thread Id: 17
#### [Ongoing] Thread Name: Thread-3, Thread Id: 19
#### [Finish] Thread Name: main, Thread Id: 1
**/

 
<예제3 코드 실행 결과>

반응형
Comments