반응형

CountDownLatch란?


쓰레드가 2개 이상일 경우 일정 개수의 쓰레드가 끝난 후 다음 쓰레드가 실행될 수 있도록 대기시키고,
끝나면, 다음 쓰레드가 실행될 수 있도록 하는 것이다.

언제 사용할 수 있을까?


나는 동시성 테스트를 할 때 사용했다. 여러가지 상황에 활용할 수 있겠지만, 테스트 코드를 작성할 때를 예를 들어보겠다.

사용 해보기

테스트용 클래스

public class CountDownLatchT {
    int count = 1;
    public void call() {
        System.out.println("count = " + this.count++);
    }
}

countDownLatch 미사용 시

@Test
void CountDownLatch() throws InterruptedException {
    ExecutorService executorService = Executors.newFixedThreadPool(5);

    CountDownLatchT tt = new CountDownLatchT();
    for (int i = 1; i <= 5; i++) {
        executorService.execute(() -> {
            tt.call();
        });
    }

    System.out.println("메인 스레드");
}

메인 스레드
count = 1
count = 3
count = 5
count = 2
count = 4

위 코드의 결과는 메인 스레드가 먼저 실행되버린다. for문이 너무 찰나의 순간이기 때문이다.

countDownLatch 사용 시

하지만 CountDownLatch를 사용하면, 아래와 같이 5개의 쓰레드가 완료된 후 메인 스레드가 실행된다.

  • new CountDownLatch(5) : 몇개의 스레드가 끝나면 다음 스레드를 시작할지 정한다. 5개를 설정했다.
  • countDownLatch.countDown() : 스레드가 끝날 때 마다 카운트를 감소한다.
  • countDownLatch.await() : 카운트가 0이되면 대기가 풀리고 이후 스레드가 실행되게 된다.
@Test
void CountDownLatch() throws InterruptedException {
    ExecutorService executorService = Executors.newFixedThreadPool(5);
    CountDownLatch countDownLatch = new CountDownLatch(5);

    CountDownLatchT tt = new CountDownLatchT();
    for (int i = 1; i <= 5; i++) {
        executorService.execute(() -> {
            tt.call();
            countDownLatch.countDown();
        });
    }

    countDownLatch.await();
    System.out.println("메인 스레드");
}

결과는 아래와 같이 확인할 수 있다.

count = 1
count = 5
count = 2
count = 3
count = 4
메인 스레드

그냥 Thread.sleep() 쓰면 안되나?


안된다.

@Test
void CountDownLatch() throws InterruptedException {
    ExecutorService executorService = Executors.newFixedThreadPool(5);

    CountDownLatchT tt = new CountDownLatchT();
    for (int i = 1; i <= 5; i++) {
        executorService.execute(() -> {
            tt.call();
        });
                Thread.sleep(1000); // 이렇게!!
    }

    System.out.println("메인 스레드");
}

이유

  1. 스레드가 정확히 작업을 언제 끝낼지도 모르는데 추측성으로 지연을 시키는 것이므로 검증이 안된다.
  2. 동시성 테스트를 하는 것인데 지연을 시키면, for문이 딜레이가 걸려서 순차적으로 스레드가 실행될 가능성이 높다.
    즉, 동시성 테스트가 아니다.

실제로 결과는 순차적으로 나온다.

count = 1
count = 2
count = 3
count = 4
count = 5
메인 스레드

그렇다면, 스레드 안에 딜레이를 주면??

for문이 아닌 스레드에 딜레이를 주는건 아무런 의미가 없다.
for문이 끝나면 바로 메인 스레드가 실행되기 때문이다.

    for (int i = 1; i <= 5; i++) {
        executorService.execute(() -> {
            tt.call();
                        Thread.sleep(1000); // 이렇게!!
        });
    }

메인 스레드
count = 3
count = 4
count = 5
count = 2
count = 1

적용 사례


Redis에 조회수를 캐싱하는데 동시에 요청할 경우 제대로 조회수가 증가되는지 테스트가 필요했다.

적용 전

그냥 for문만 돌렸다. 어떻게 보면, 틀린건 아니다.
왜냐하면, 동시에 요청은 제대로 들어갔고 이후에 메인 스레드가 실행되었기 때문이다.
스레드들의 시간이 얼마 걸리지 않았기 때문이다.

하지만 빈틈은 있다. increaseHits()가 좀 오래걸리는 테스트였다면, 메인 스레드가 먼저 실행됐을 것이다.
운 좋게 순간적인 작업이었기 때문에 성공하는 테스트가 된 것이다.

@Test
void 동시성_테스트() {
    for (int i = 1; i <= 10_000; i++) {
        Runnable run = () -> hitsRedisRepository.increaseHits(1L);
        new Thread(run).start();
    }

    Integer hits = hitsRedisRepository.getHits(1L);
    assertThat(hits).isEqualTo(10_000);
}

적용 후

처음에 설명한 바와 같이 적용했다. 이러면, 메서드가 작업이 좀 걸린다 해도, 메인 스레드는 대기하기 때문에
정확한 테스트가 된다.

@Test
void 조회수_증가_동시성_테스트() throws InterruptedException {
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    CountDownLatch countDownLatch = new CountDownLatch(10);
    for (int i = 1; i <= 10; i++) {
        executorService.execute(() -> {
            hitsRedisRepository.incrementHits(1L);
            countDownLatch.countDown();
        });
    }

    countDownLatch.await();
    Integer hits = hitsRedisRepository.getHits(1L);
    assertThat(hits).isEqualTo(10);
}

(ExecutorService를 알게되어 Thread를 사용하지 않게 변경했다.)

참고


병행성(Concurrency)을 위한 CountDownLatch

반응형
복사했습니다!