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("메인 스레드");
}
이유
- 스레드가 정확히 작업을 언제 끝낼지도 모르는데 추측성으로 지연을 시키는 것이므로 검증이 안된다.
- 동시성 테스트를 하는 것인데 지연을 시키면, 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를 사용하지 않게 변경했다.)
참고
'Java' 카테고리의 다른 글
Java static과 클래스 로드 및 초기화 (0) | 2022.08.28 |
---|---|
JDBC란 무엇인가? (0) | 2022.08.17 |
Java Comparator, Comparable (0) | 2022.02.22 |
리플렉션으로 DI 컨테이너 만들기 (0) | 2021.12.21 |
일급 컬렉션 - 비지니스에 종속적인 자료구조 (0) | 2021.12.03 |