JAVA/비동기 프로그래밍

[Java] ThreadPoolExecutor

꾸준함. 2024. 4. 28. 02:50

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() 메서드를 통해 미리 생성 가능

 

https://blog.csdn.net/qq_67394089/article/details/129533697

 

 

 

2. keepAliveTime

  • corePoolSize보다 많은 쓰레드가 존재하는 경우 각 쓰레드가 keepAliveTime보다 오랜 시간 동안 유휴 상태였다면 해당 쓰레드는 종료
  • keep alive 정책은 corePoolSize 쓰레드보다 많은 쓰레드가 있을 때만 적용되지만 allowCoreThreadTimeOut(boolean) 메서드를 사용하여 corePoolSize 내 core 쓰레드에도 적용 가능
  • Executors.newCachedThreadPool()를 통해 풀이 생성된 경우 대기 제한 시간이 60초
    • 풀에 있는 쓰레드 중 대기 상태가 60초 이상 지속되면 해당 쓰레드는 제거

 

  • Executors.newFixedThreadPool()을 통해 풀이 생성된 경우 대기 제한 시간이 없음

 

keep-alive time 설명

 

한글 의역

  • 대기 제한 시간(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] - 정수원 강사님

반응형