Executor 스레드 풀 관리

실무에서 대량의 요청을 효율적으로 처리하려면 스레드 풀의 동작 원리를 정확히 이해해야 한다.

ThreadPoolExecutor 핵심 파라미터

ExecutorService es = new ThreadPoolExecutor(
    2,                          // corePoolSize
    4,                          // maximumPoolSize
    3000,                       // keepAliveTime
    TimeUnit.MILLISECONDS,      // unit
    new ArrayBlockingQueue<>(2) // workQueue
);

파라미터 설명

  • corePoolSize: 기본 스레드 수 (항상 유지)
  • maximumPoolSize: 최대 스레드 수 (긴급 상황 시 확장)
  • keepAliveTime: 초과 스레드 생존 시간
  • workQueue: 작업 대기 큐

초과 스레드 (Excess Thread)

  • 초과 스레드: maximumPoolSize – corePoolSize
  • 예시: 4 – 2 = 2개의 초과 스레드

스레드 풀 동작 원리

작업 처리 우선순위

BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);
ExecutorService es = new ThreadPoolExecutor(2, 4, 3000, 
    TimeUnit.MILLISECONDS, workQueue);

처리 순서

  • corePoolSize 까지 스레드 생성 → 작업 즉시 실행
  • core 초과 시 큐에 저장 → 대기
  • 큐 가득 차면 maximumPoolSize까지 스레드 생성 → 긴급 처리
  • maximum 초과 시 거절RejectedExecutionException

단계별 실행 분석

// task1 제출
es.execute(new RunnableTask("task1"));
// → pool=1, active=1, queuedTasks=0
// → 스레드1 생성 및 즉시 실행

// task2 제출
es.execute(new RunnableTask("task2"));
// → pool=2, active=2, queuedTasks=0
// → 스레드2 생성 및 즉시 실행 (core 도달)

// task3 제출
es.execute(new RunnableTask("task3"));
// → pool=2, active=2, queuedTasks=1
// → 큐에 저장 (core는 가득, 큐는 여유)

// task4 제출
es.execute(new RunnableTask("task4"));
// → pool=2, active=2, queuedTasks=2
// → 큐에 저장 (큐 가득)

// task5 제출 - 중요!
es.execute(new RunnableTask("task5"));
// → pool=3, active=3, queuedTasks=2
// → 초과 스레드3 생성! (큐 가득 = 긴급 상황)
// → 큐에 넣지 않고 즉시 실행

// task6 제출
es.execute(new RunnableTask("task6"));
// → pool=4, active=4, queuedTasks=2
// → 초과 스레드4 생성 (maximum 도달)

// task7 제출
try {
    es.execute(new RunnableTask("task7"));
} catch (RejectedExecutionException e) {
    log("task7 실행 거절: " + e);
}
// → RejectedExecutionException 발생
// → 큐 가득 + maximum 도달 = 처리 불가

핵심 메커니즘

스레드 생성 전략

1단계: task1 → 스레드1 생성 후 즉시 처리
       (큐에 넣지 않음, 이미 처리할 스레드 있음)

2단계: task2 → 스레드2 생성 후 즉시 처리
       (corePoolSize 도달)

3단계: task3, task4 → 큐에 저장
       (core 가득, 큐 사용)

4단계: task5, task6 → 초과 스레드 생성 후 즉시 처리
       (큐 가득 = 긴급!, 큐에 넣을 수 없음)

5단계: task7 → 거절
       (큐 가득 + maximum 도달)

왜 큐가 가득 차야 초과 스레드가 생성될까?

설계 의도

  • 큐에 여유 → 정상 상황 → core 스레드로 처리
  • 큐 가득 → 긴급 상황 → 추가 리소스(초과 스레드)투입
  • 시스템 자원을 필요할 때만 확장 (효율성)

긴급 상황 판단 기준

if (큐.isFull() && pool < maximumPoolSize) {
    // 대기 작업 적체 → 처리 속도 부족
    // 초과 스레드 생성으로 처리 속도 향상
    createExcessThread();
}

초과 스레드 생명주기

// 3초간 작업 없으면 제거
keepAliveTime = 3000, TimeUnit.MILLISECONDS

동작 방식

  • 초과 스레드 작업 완료
  • 큐에서 새 작업 대기 (3초 타임아웃)
  • 3초 내 작업 도착 → 처리 후 타이머 리셋
  • 3초 내 작업 없음 → 스레드 제거

리셋 메커니즘

  • 스레드3 작업 완료 → 타이머 시작 (3초)
  • 2초 후 작업 도착 → 작업 처리
  • 작업 완료 → 타이머 리셋 (다시 3초)
  • 3초간 작업 없음 → 스레드 제거

작업 처리 흐름

큐에서 작업 가져오기

// 스레드1, 스레드5 작업 완료
thread1.complete(task1);
thread5.complete(task5);

// 블로킹 큐에서 대기 중인 작업 획득
task3 = workQueue.take(); // 스레드1이 획득
task4 = workQueue.take(); // 스레드3이 획득

// 즉시 실행
thread1.execute(task3);
thread3.execute(task4);

완료 후 상태

// 모든 작업 완료 후
sleep(3000);
log("== 작업 수행 완료 ==");
printState(es);
// → pool=4, active=0, queuedTasks=0, completedTasks=6

// 3초 후 (keepAliveTime 초과)
sleep(3000);
log("== maximumPoolSize 대기 시간 초과 ==");
printState(es);
// → pool=2, active=0, queuedTasks=0, completedTasks=6
// → 초과 스레드(3, 4) 제거됨

실행 결과 해석

[pool=0, active=0, queuedTasks=0, completedTasks=0]  // 초기 상태
task1 -> [pool=1, active=1, queuedTasks=0, ...]      // 스레드1 생성
task2 -> [pool=2, active=2, queuedTasks=0, ...]      // 스레드2 생성
task3 -> [pool=2, active=2, queuedTasks=1, ...]      // 큐 저장
task4 -> [pool=2, active=2, queuedTasks=2, ...]      // 큐 저장
task5 -> [pool=3, active=3, queuedTasks=2, ...]      // 초과 스레드3
task6 -> [pool=4, active=4, queuedTasks=2, ...]      // 초과 스레드4
task7 실행 거절 예외 발생                             // 처리 불가

== 작업 수행 완료 ==
[pool=4, active=0, queuedTasks=0, completedTasks=6]  // 초과 스레드 대기

== maximumPoolSize 대기 시간 초과 ==
[pool=2, active=0, queuedTasks=0, completedTasks=6]  // 초과 스레드 제거

핵심 정리

작업 처리 우선 순위 (암기가 필요)

  • corePoolSize까지 스레드 생성 → 새 스레드 생성 후 즉시 실행
  • corePoolSize 초과 → 큐에 작업 저장
  • 큐 가득 참 → maximumPoolSize까지 초과 스레드 생성 → 긴급 상황 시 즉시 실행
  • maximumPoolSize 초과 → RejectedExecutionException → 작업 거절

설계 원칙

  • 효율성: 필요한 때만 리소스 확장
  • 긴급 대응:적체 시 초과 스레드 투입
  • 자원 절약: 긴급 상황 종료 시 초과 스레드 제거
  • 명확한 한계: maximum 초과 시 명시적 거절

실무 고려 사항

큐 크기 설정

  • 너무 작을 경우 → 자주 초과 스레드 생성 (오버헤드)
  • 너무 클 경우 → 메모리 부담, 긴급 상황 감지 지연

keepAliveTime 설정

  • 짧을 경우 → 빈번한 생성/제거 (오버헤드)
  • 길 경우 → 불필요한 리소스 점유

출처 – 김영한 님의 강의 중 김영한의 실전 자바 – 고급 1편, 멀티스레드와 동시성