실무에서 대량의 요청을 효율적으로 처리하려면 스레드 풀의 동작 원리를 정확히 이해해야 한다.
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 설정
- 짧을 경우 → 빈번한 생성/제거 (오버헤드)
- 길 경우 → 불필요한 리소스 점유