Thread Pool & Executor Framework

나무나무·2025년 9월 21일

자바

목록 보기
6/6

스레드 풀(Thread Pool)


스레드 직접 사용

  • 스레드 생성 비용으로 인한 성능 문제 → 스레드는 매우 무거운 객체
    • 자신만의 호출 스택을 가지고 있어야 함 → 호출 스택을 위한 메모리 할당
    • 운영체제 자원 사용 → CPU와 메모리 리소스를 소모하는 작업
    • 운영체제 스케줄러 설정 → 스레드 관리, 실행 순서 조정에 추가 오버헤드 발생 가능함
    • 가벼운 작업은 작업 실행 시간보다 스레드 생성 시간이 더 오래 걸릴 수 있음
  • 스레드 관리 문제
    • 메모리 자원, 서버 CPU가 한정되어 있음 → 스레드는 무한히 만들 수 없음
    • 최대 스레드 수까지만 스레드를 생성하고 관리할 수 있어야 함
    • 스레드 종료 시에도 남은 작업을 모두 수행하고 종료되어야 함 → 이 경우도 어딘가에서 스레드를 관리해줘야 함
  • Runnable 인터페이스의 불편함
    • 반환 값이 없음
    • 예외 처리 → 체크 예외의 처리는 메서드 내부에서 처리해야 함

스레드 풀(pool)

  • 스레드를 필요한 만큼 미리 만들어둠 → 작업 요청 시 스레드를 하나 조회해 처리한 뒤, 스레드를 다시 반납
  • 스레드 보관, 재사용 가능 + 스레드 생성 시간 절약
  • Executor → 자바가 제공하는 프레임 워크 / 스레드 풀, 스레드 관리, 생산자 소비자 문제 해결

Executor 프레임워크

  • 멀티 스레딩 및 병렬 처리를 돕는 기능 모음
  • 작업 실행 관리 및 풀 관리를 효율적으로 처리, 개발자가 직접 스레드 생성 및 관리까지

Future


Runnable vs Callable

➡️ Runnable

  • 반환 타입 void()
  • 예외가 선언되어 있지 않음 → 체크 예외를 던질 수 없음 (런타임 예외는 제외)

➡️ Callable

  • concurrent에서 제공되는 기능
  • 반환 타입이 제네릭 V 임. → 값의 반환 가능
  • throws Exception 예외가 선언되어 있음 → 해당 인터페이스를 구현하는 모든 메서드는 체크 예외인 Exception과 그 하위 예외를 모두 던질 수 있음

Future 사용

  • ExecutorService es = new ThreadPoolExecutor(1,1,0, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
    - corePoolSize: 스레드 풀에서 관리되는 기본 스레드 수
    - maximumPoolSize: 스레드 풀에서 관리되는 최대 스레드 수
    - keepAliveTime: 기본 스레드 수 초과해서 만들어진 스레드가 생존할 수 있는 대기 시간
    - BlockingQueue: 작업을 보관 → 생산자, 소비자 문제를 해결 → 작업을 무한대로 저장 가능
  • ExecutorService es = Executors.newFixedThreadPool(1); : 편의 코드
  • Future는 전달한 작업의 미래 결과를 담고 있다고 생각하면 됨

💡 왜 Future를 써야 할까?

  • 요청스레드가 작업을 요청한 뒤 다른 작업 수행 가능
  • 요청 스레드가 필요한 요청을 전부 한 다음에 get()을 통해 블로킹 상태로 대기하면서 결과를 받으면 된다.

Future 메소드

  • boolean cancel(boolean mayInterruptIfRunning)
    • 작업이 실행중이 아니거나 시작 전인 경우 취소, 실행 중인 경우 mayInterruptIfRunningtrue면 중단
    • 작업이 성공적으로 취소된 경우 true, 이미 완료되었거나 취소할 수 없으면 false
  • boolean isCancelled()
    • 작업이 취소되었는지 여부 확인
  • boolean isDone()
    • 작업이 완료되었는지 여부 확인
  • State state()
    • 상태 반환(RUNNING , SUCCESS , FAILED , CANCELLED)
  • V get()
    • 작업 완료까지 대기, 완료되면 결과 반환
    • 예외
      • InterruptedException : 대기 중인 스레드에 인터럽트 발생
      • ExecutionException : 작업 계산 중 예외 발생
  • V get(long timeout, TimeUnit unit)
    • 시간 초과 시 예외 발생
    • 예외
      • InterruptedException : 대기 중인 스레드에 인터럽트 발생
      • ExecutionException : 작업 계산 중 예외 발생
      • TimeoutException : 주어진 시간 내에 작업 완료되지 않음

Future 취소

  • future.cancel(true) : Future를 취소 상태로 변경 → 작업이 실행중인 경우 인터럽트 발생, 작업 중단
  • future.cancel(false) : Future를 취소 상태로 변경 → 이미 실행중인 작업은 중단하지 않음

Future 예외

  • Future.get() 는 작업의 결과를 받을 수도, 예외를 받을 수도 있음

Executor


ExecutorService

  • Executor 인터페이스 확장, 작업의 제출과 제어 기능 추가 제공
  • ExecutorService 인터페이스의 기본 구현체는 ThreadPoolExecutor
  • 주요 메서드 : execute() - 반환값 x, submit() - 반환값 o, invokeAll(), invokeAny(), shutdown(), close() - ExecutorService 종료

ExecutorService - 작업 컬렉션 처리

  • invokeAll() : 모든 Callable 작업을 제출, 모든 작업이 완료될 때까지 기다림
  • invokeAny() : 가장 먼저 완료된 작업 결과 반환, 나머지는 전부 취소

ExecutorService- 종료

  • 서비스 종료
    • void shutdown() : 새로운 작업을 받지 않고 제출된 작업을 모두 완료 후 종료
    • List<Runnable> shutdownNow() : 실행 중인 작업 모두 중단(인터럽트 발생) / 큐를 비우면서 큐에 담긴 작업을 꺼내 컬렉션으로 반환
  • 서비스 상태 확인
    • boolean isShutdown()
    • boolean isTerminated()
  • 작업 완료 대기
    • boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException : 서비스 종료 시 모든 작업이 완료될 때까지 대기
  • close()
    • shutdown()을 호출하고, 작업이 완료되거나 인터럽트가 발생할 때 까지 무한정 반복 대기

Executor 스레드 풀 관리

  • 작업 요청 시 core 사이즈 만큼 스레드 생성
  • core 사이즈 초과 시 작업을 큐에 넣음
  • 큐를 초과할 경우 max 사이즈 만큼 스레드 생성 → “초과 스레드”
  • max 사이즈도 초과할 경우 예외가 발생함

Executor 전략

➡️ 고정 스레드 풀 전략

 ExecutorService es = Executors.newFixedThreadPool(2);
 ThreadPoolExecutor es = new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())
  • newFixedThreadPool(nThreads)
  • 초과 스레드는 생성하지 않음
  • 큐 사이즈에 제한이 없음
  • 스레드 수 고정 → CPU, 메모리 리소스가 어느 정도 예측 가능한 안정적인 방식

💡 요청은 제한 없이 무한정 받을 수 있는데, 처리하는 스레드 수는 한정적이라 사용자가 늘어날 수록 응답 속도가 떨어지게 됨


➡️ 캐시 스레드 풀 전략

  ExecutorService es = Executors.newCachedThreadPool();
  ThreadPoolExecutor es = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 3, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
  • newCachedThreadPool();
  • 기본 스레드 사용x , 60초 생존 주기를 갖는 초과 스레드만 사용
  • 큐에 작업을 저장하지 않음(SynchronousQueue ) → 생산자의 요청을 스레드 풀의 소비자가 직접 받아서 바로 처리
  • 모든 요청이 대기하지 않고 스레드가 바로바로 처리함 → 빠른 처리 가능

💡사용자 증가에 따라 스레드 사용량도 늘어남 → CPU 메모리 사용량 증가 → 메모리 자원의 한계 때문에 적절한 시점에 시스템을 증설해야 함. 안그러면 다운됨


➡️ 사용자 정의 풀 전략

 ExecutorService es = new ThreadPoolExecutor(100, 200, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000));
 
  • 일반 - 일반적인 상황에는 고정 크기 스레드로 서비를 안정적으로 운영
  • 긴급 - 사용자 요청이 증가 시 → 초과 스레드 투입, 작업 빠르게 처리
  • 거절 - 사용자 요청의 폭증, 긴급 대응도 어려울 경우 → 요청 거절

💡큐가 가득 차야 긴급 상황으로 인지됨 → 큐를 무한대 사이즈로 사용하면 큐가 가득차지 않아 무한대의 작업을 처리해야 하는 상황 발생


Executor 예외 정책

➡️ AbortPolicy

 ThreadPoolExecutor es = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, new SynchronousQueue<>(), new ThreadPoolExecutor.AbortPolicy());
  • 작업 거절, RejectedExecutionException 예외 던짐
  • 기본 정책으로 생략 가능

➡️ DiscardPolicy

 ThreadPoolExecutor es = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, new SynchronousQueue<>(), new ThreadPoolExecutor.DiscardPolicy());
  • 거절된 작업 무시, 아무런 예외 발생 x

➡️ CallerRunsPolicy

 ThreadPoolExecutor es = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, new SynchronousQueue<>(), new ThreadPoolExecutor.CallerRunsPolicy());
  • 호출한 스레드가 직접 작업을 수행함
  • 생산자 스레드가 대신 일을 수행하기 때문에 생산 자체가 느려짐 → 생산 속도의 조절이 가능함

➡️ 사용자 정의

  • RejectedExecutionHandler 인터페이스 구현
  • 자신만의 거절 처리 전략을 정의할 수 있음
profile
백엔드 개발자 나무입니다

0개의 댓글