ThreadPoolExecutor는 ExecutorService를 구현한 클래스로서 매개변수를 통해 다양한 설정과 조정이 가능하며 사용자가 직접 컨트롤할 수 있는 쓰레드 풀입니다.
- 기존의 Executors가 생성하는 ThreadPool은 옵션 세부 튜닝이 어려웠던 반면 ThreadPoolExecutor는 이를 보완함
ThreadPoolExecutor는 다양한 구성 옵션을 통해 동작을 조정할 수 있으며 주요 구성 요소는 다음과 같습니다.
- corePoolSize
- maximumPoolSize
- keepAliveTime
- BlockingQueue
- RejectedExecutionHandler
- ThreadPoolExecutor Hook
1. corePoolSize & maximumPoolSize
- ThreadPoolExecutor는 corePoolSize 및 maximumPoolSize로 설정된 개수에 따라 풀 크기를 자동으로 조정
- setCorePoolSize(), setMaximumPoolSize() 메서드를 통해 corePoolSize 및 maximumPoolSize를 동적으로 변경 가능
- ThreadPoolExecutor는 새 작업이 추가될 때 corePoolSize 미만의 쓰레드가 실행 중이라면 corePoolSize가 될 때까지 새 쓰레드를 생성
- corePoolSize를 초과할 경우 큐 사이즈가 남아 있을 경우 큐에 작업을 추가하고
- 큐가 가득 차 있는 경우 maximumPoolSize가 될 때까지 새 쓰레드를 생성
- 기본적으로 쓰레드 풀은 쓰레드를 미리 생성하지 않고 새 작업이 도착할 때만 생성
- prestartCoreThread() 혹은 prestartAllCoreThreads() 메서드를 통해 미리 생성 가능
2. keepAliveTime
- corePoolSize보다 많은 쓰레드가 존재하는 경우 각 쓰레드가 keepAliveTime보다 오랜 시간 동안 유휴 상태였다면 해당 쓰레드는 종료됨
- keep alive 정책은 corePoolSize 쓰레드보다 많은 쓰레드가 있을 때만 적용되지만 allowCoreThreadTimeOut(boolean) 메서드를 사용하여 corePoolSize 내 core 쓰레드에도 적용 가능
- Executors.newCachedThreadPool()를 통해 풀이 생성된 경우 대기 제한 시간이 60초
- 풀에 있는 쓰레드 중 대기 상태가 60초 이상 지속되면 해당 쓰레드는 제거
- Executors.newFixedThreadPool()을 통해 풀이 생성된 경우 대기 제한 시간이 없음
한글 의역
- 대기 제한 시간(Keep-alive time)은 쓰레드 풀이 현재 코어 쓰레드 수(corePoolSize) 보다 더 많은 쓰레드를 가지고 있는 경우, 이 쓰레드들이 keepAliveTime 동안 아무 작업을 하지 않으면 쓰레드 풀에서 제거
- 쓰레드 풀이 활발하게 사용되지 않을 때 자원 소모를 줄이는 수단으로 제공
- 나중에 풀이 더 활발해지면 새로운 쓰레드가 생성
- 해당 매개변수는 ThreadPoolExecutor.setKeepAliveTime 메서드를 사용하여 동적으로 변경 가능
- TimeUnit.NANOSECONDS의 Long.MAX_VALUE 값을 사용하면 쓰레드가 종료되기 전에 언제 까지든 대기 중인 쓰레드를 비활성화 가능
3. BlockingQueue
- 앞서 언급했다시피 기본적으로 쓰레드 풀은 작업이 제출되면 corePoolSize의 새 쓰레드를 추가해서 작업을 할당하고 큐에 작업을 바로 추가하지 않음
- corePoolSize를 초과한 상태로 쓰레드가 실행 중일 경우 새 쓰레드를 추가해서 작업을 할당하는 대신 큐가 가득찰 때까지 큐에 작업을 추가
- 큐에 공간이 가득차게되고 쓰레드가 maxPoolSize 이상 실행 중일 경우 더 이상의 작업은 추가되지 않고 RejectedExecutionHandler가 실행 (앞선 gif 참고)
- BlockingQueue의 종류는 크게 세 가지
- SynchronousQueue
- LinkedBlockingQueue
- ArrayBlockingQueue
3.1 SynchronousQueue
- Executors의 newCachedThreadPool에서 사용
- 내부적으로 크기가 0인 큐로써 쓰레드 간 작업을 직접 전달하는 역할
- 작업을 대기열에 넣으려고 할 때 실행할 쓰레드가 즉시 없으면 새로운 쓰레드 생성
- 요소를 추가하려고 하면 다른 쓰레드가 해당 요소를 꺼낼 때까지 현재 쓰레드는 블로킹되고 요소를 꺼내려고 하면 다른 쓰레드가 요소를 추가할 때까지 현재 쓰레드는 블로킹
- SynchronousQueue는 평균적인 처리보다 더 빨리 작업이 요청되면 쓰레드가 무한정 증가할 수 있음
3.2 ArrayBlockingQueue
- 내부적으로 고정된 크기의 배열을 사용하여 작업을 추가
- 큐를 생성할 때 최대 크기를 지정해야하며 한 번 지정된 큐의 크기는 변경할 수 없음
- ArrayBlockingQueue의 크기가 큰데 작은 풀을 사용하면 CPU 사용량, OS 리소스, 그리고 context switching 오버헤드가 최소화되지만 낮은 처리량 유발할 수 있음
- ArrayBlockingQueue의 크기가 작은데 큰 풀을 사용하면 CPU 사용률이 높아지지만 대기열이 가득 찰 경우 추가적인 작업을 거부하기 때문에 처리량이 감소할 수 있음
3.3 LinkedBlockingQueue
- Executors의 newFixedThreadPool()에서 사용
- 무제한 크기의 큐로써 corePoolSize의 쓰레드가 모두 사용 중인 경우 새로운 작업이 제출될 경우 대기열에 등록하고 대기
- 무제한 크기의 큐이기 때문에 corePoolSize의 쓰레드만 생성하고 더 이상 추가 쓰레드를 생성하지 않음
- maximumPoolSize를 설정해도 아무런 효과 없음
- LinkedBlockingQueue 방식은 일시적인 요청의 폭증을 완화하는데 유용
- 평균적인 처리보다 더 빨리 작업이 도착할 경우 대기열이 무한정 증가할 수 있음
3.3.1 LinkedBlockingQueue의 주요 메서드
메서드 | return 값 | blocking 여부 | 예외 처리 |
add(E e) | 성공: true 실패 : 예외 발생 |
non-blocking | IllegalStateException |
offer(E e) | 성공: true 실패: false |
non-blocking | InterruptedException |
put(E e) | X | blocking | X |
remove() | 큐에서 요소를 제거하고 반환 큐가 비어있을 경우 예외 발생 |
non-blocking | NoSuchElementException |
poll() | 큐에서 요소를 제거하고 반환 큐가 비어있을 경우 null 반환 |
non-blocking | X |
take() | 큐에서 요소를 제거하고 반환 큐가 비어있을 경우 blocking |
blocking | InterruptedException |
* take()의 경우 무기한 대기가 발생할 수 있기 때문에 타임아웃을 지원하는 다른 메서드를 활용하는 것을 권장
4. RejectedExecutionHandler
- execute(Runnable) 메서드를 통해 제출된 작업이 작업 풀의 포화로 인해 거부 될 경우 execute 메서드는 RejectedExecutionHandler의 rejectedExecution() 메서드를 호출
- 미리 정의된 네 가지 핸들러 정책 클래스가 제공되며 직접 사용자 정의 클래스를 만들어 사용
- ThreadPoolExecutor.AbortPolicy
- ThreadPoolExecutor.CallerRunsPolicy
- ThreadPoolExecutor.DiscardPolicy
- ThreadPoolExecutor.DiscardOldestPolicy
4.1 ThreadPoolExecutor.AbortPolicy
- 기본 정책
- 작업 거부 시 RejectedExecutionException 예외 발생
4.2 ThreadPoolExecutor.CallerRunsPolicy
- Executor가 종료되지 않은 경우 execute를 호출한 쓰레드 자체가 작업을 실행
4.3 ThreadPoolExecutor.DiscardPolicy
- 거부된 작업은 그냥 삭제됨
4.4 ThreadPoolExecutor.DiscardOldestPolicy
- Executor가 종료되지 않은 경우 대기열의 맨 앞에 있는 작업을 삭제하고 실행을 재시도
- 재시도 실패 시 반복될 수 있음
* 다음과 같이 CustomRejectedExecutionHandler도 정의 가능
5. ThreadPoolExecutor Hook
- ThreadPoolExecutor 클래스는 쓰레드 풀을 관리하고 작업 실행 시점에 특정 이벤트를 처리하기 위한 Hook 메서드 제공
- ThreadPoolExecutor 클래스를 상속할 경우 3개의 Hook 메서드를 재정의 가능
- beforeExecute(Thread thread, Runnable runnable)
- afterExecute(Runnable runnable, Throwable throwable)
- terminated()
5.1 beforeExecute(Thread thread, Runnable runnable)
- 작업 쓰레드가 작업을 실행하기 전에 호출되는 메서드
- 제출된 각 작업마다 한 번씩 호출되고 작업 실행 전에 원하는 동작을 추가 가능
5.2 afterExecute(Runnable runnable, Throwable throwable)
- 작업 쓰레드가 작업 실행을 완료한 후에 호출되는 메서드
- 제출된 각 작업마다 한 번씩 호출되고 작업 실행 후에 원하는 동작 추가 가능
- 작업 실행 후 예외 처리도 수행 가능
5.3 terminated()
- 쓰레드 풀이 완전히 종료된 후 호출되는 메서드
- 쓰레드 풀이 종료되면 해당 메서드를 재정의하여 clean-up 작업 수행 가능
6. ThreadPoolExecutor 생명 주기와 상태
- ThreadPoolExecutor는 다양한 생명 주기와 상태를 가지며 상태에 따라 작업 쓰레드 풀의 동작이 결정됨
- 주요 상태는 다음과 같음
- RUNNING
- SHUTDOWN
- STOP
- TIDYING
- TERMINATED
- 주요 상태 전환은 다음과 같음
- RUNNING > SHUTDOWN
- SHUTDOWN > STOP
- SHUTDOWN > TIDYING
- STOP > TIDYING
- TIDYING > TERMINATED
6.1 주요 상태
6.1.1 RUNNING
- 새 작업을 수용하고 대기 중인 작업을 처리
- 쓰레드 풀이 생성된 직후 RUNNING 상태
6.1.2 SHUTDOWN
- 쓰레드 풀을 종료하는 상태
- 새 작업을 수용하지 않지만 대기 중인 작업은 처리
6.1.3 STOP
- 쓰레드 풀을 종료하는 상태
- 새 작업을 수용하지 않고 대기 중인 작업도 처리하지 않음
- 현재 진행 중인 작업 또한 중단 시킴
6.1.4 TIDYING
- 모든 작업이 종료되었으며 workerCount가 0인 상태
- terminated() hook 메서드를 실행
6.1.5 TERMINATED
- terminated() 메서드가 완료되었고 쓰레드 풀이 종료된 상태
6.2 주요 상태 전환
6.2.1 RUNNING > SHUTDOWN
- shutdown() 메서드가 호출될 때 쓰레드 풀은 종료 상태로 전환
6.2.2 SHUTDOWN > STOP
- shutdownNow() 메서드가 호출될 때 쓰레드 풀의 상태는 SHUTDOWN에서 STOP 상태로 전환
6.2.3 SHUTDOWN > TIDYING
- 큐와 풀이 모두 비어있을 경우 SHUTDOWN 상태에서 TIDYING 상태로 전환
6.2.4 STOP > TIDYING
- 풀이 비어있을 때 STOP 상태에서 TIDYING 상태로 전환
6.2.5 TIDYING > TERMINATED
- terminated() hook 메서드가 실행되고 완료될 경우 TIDYING 상태에서 TERMINATED 상태로 전환
정리
참고
자바 동시성 프로그래밍 [리액티브 프로그래밍 Part.1] - 정수원 강사님
'JAVA > 비동기 프로그래밍' 카테고리의 다른 글
[Java] 자바 IO, NIO, AIO (0) | 2024.07.14 |
---|---|
[Java] CompletableFuture (0) | 2024.05.12 |
[Java] 자바 동시성 프레임워크 (4) | 2024.04.20 |
[Java] 동기화 도구 (0) | 2024.03.14 |
[Java] Lock, ReentrantLock, ReadWriteLock, ReentrantReadWriteLock (0) | 2024.03.09 |