매번 new Thread()로 스레드를 생성하면 두 가지 문제가 있다.
문제 1: 스레드 생성/소멸 비용
요청 올 때마다:
new Thread() → OS에 스레드 생성 요청 → 작업 완료 → 스레드 소멸
스레드 생성/소멸은 OS 레벨 작업이라 비용이 크다.
문제 2: 스레드 수 제어 불가
요청 1 → new Thread()
요청 2 → new Thread()
...
요청 10000 → new Thread() → 메모리 고갈 → 서버 다운
요청이 폭발하면 스레드가 무한정 생성돼서 서버가 죽는다.
스레드 풀은 이 두 문제를 해결한다.
미리 스레드를 N개 만들어두고
작업이 오면 놀고 있는 스레드에 할당
작업 끝나면 스레드를 소멸시키지 않고 재사용
스레드가 다 바쁘면 작업을 큐에서 대기
Java에서 스레드 풀을 다루는 인터페이스다. Executors 클래스의 팩토리 메서드로 생성한다.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolTest {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3); // 스레드 3개 고정
for (int i = 0; i < 10; i++) {
int taskNum = i;
executor.submit(() -> {
System.out.println("작업 " + taskNum + " 실행: " + Thread.currentThread().getName());
});
}
executor.shutdown();
}
}
실행 결과:
작업 1 실행: pool-1-thread-2
작업 2 실행: pool-1-thread-3
작업 0 실행: pool-1-thread-1
작업 3 실행: pool-1-thread-2
작업 4 실행: pool-1-thread-3
...
스레드 이름이 pool-1-thread-1, pool-1-thread-2, pool-1-thread-3 딱 3종류다.
10개의 작업을 3개의 스레드가 나눠서 재사용하며 처리했다.
submit()에 넘길 수 있는 작업 단위는 두 가지다.
| Runnable | Callable | |
|---|---|---|
| 반환값 | 없음 (void) | 있음 (제네릭 타입) |
| 예외 | checked 예외 선언 불가 | checked 예외 throws 가능 |
| 결과 수령 | 불가 | Future로 수령 |
// Runnable — 반환값 없음
executor.submit(() -> {
System.out.println("작업 실행");
// return 없음
});
// Callable — 반환값 있음
Future<String> future = executor.submit(() -> {
Thread.sleep(1000); // checked 예외를 throws 없이 사용 가능
return "작업 결과"; // 반환값 있음
});
String result = future.get(); // 작업이 완료될 때까지 블로킹 대기
System.out.println(result); // "작업 결과"
submit()의 시그니처:
Future<?> submit(Runnable task) // Runnable — 반환값 없음
<T> Future<T> submit(Callable<T> task) // Callable — 반환값 있음
Lambda를 넘길 때 컴파일러는 반환값 여부로 어떤 인터페이스인지 추론한다.
// 반환값 없음 → Runnable로 추론
executor.submit(() -> System.out.println("실행"));
// 반환값 있음 → Callable<String>으로 추론
executor.submit(() -> "결과값");
Future<Integer> future = executor.submit(() -> {
Thread.sleep(2000);
return 42;
});
// 다른 작업 수행 가능 (비동기)
System.out.println("다른 작업 중...");
Integer result = future.get(); // 2초 후 결과 반환 (블로킹)
System.out.println("결과: " + result); // 결과: 42
future.get()은 작업이 완료될 때까지 호출한 스레드를 블로킹한다.
타임아웃도 지정할 수 있다.
Integer result = future.get(3, TimeUnit.SECONDS); // 3초 안에 결과 없으면 TimeoutException
각 팩토리 메서드는 내부적으로 ThreadPoolExecutor를 다른 파라미터로 생성한다.
ThreadPoolExecutor의 핵심 파라미터:
corePoolSize : 기본적으로 유지할 스레드 수 (작업이 없어도 유지)
maximumPoolSize : 최대로 생성할 수 있는 스레드 수
keepAliveTime : corePoolSize 초과 스레드가 idle 상태일 때 살아있는 시간
ExecutorService executor = Executors.newFixedThreadPool(3);
초기 스레드 수 : 0
코어 스레드 수 : nThreads (3)
최대 스레드 수 : nThreads (3)
keepAliveTime : 0ms
작업 1~3 → 스레드 3개 생성되어 처리
작업 4~10 → 큐에 쌓여서 스레드가 빌 때까지 대기
ExecutorService executor = Executors.newCachedThreadPool();
초기 스레드 수 : 0
코어 스레드 수 : 0
최대 스레드 수 : Integer.MAX_VALUE (사실상 무제한)
keepAliveTime : 60초
작업 1 → 스레드 없음 → 새 스레드 생성
작업 2 → 스레드1이 바쁨 → 새 스레드 생성
작업 1 완료 → 스레드1 idle 상태 (60초 유지)
작업 3 → idle 스레드1 재사용
스레드1 60초 idle → 소멸
ExecutorService executor = Executors.newSingleThreadExecutor();
초기 스레드 수 : 0
코어 스레드 수 : 1
최대 스레드 수 : 1
keepAliveTime : 0ms
스레드 풀을 종료하지 않으면 JVM이 종료되지 않는다. 스레드 풀 내부의 스레드들이 일반 스레드(비데몬)이기 때문이다.
executor.shutdown();
submit() 호출 시 RejectedExecutionException)executor.shutdown();
try {
// 최대 10초 기다림
if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
executor.shutdownNow(); // 10초 안에 안 끝나면 강제 종료
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
List<Runnable> notExecuted = executor.shutdownNow();
// shutdownNow()가 interrupt()를 보내도 이 스레드는 무시함
executor.submit(() -> {
while (true) {
// InterruptedException 안 나는 루프 → interrupt 무시됨
}
});
executor.shutdownNow(); // 종료 시도하지만 위 스레드는 계속 실행됨
executor.isShutdown(); // shutdown() 또는 shutdownNow() 호출 여부
executor.isTerminated(); // 모든 스레드가 실제로 종료됐는지 여부
shutdown() 호출 직후:
isShutdown() → true
isTerminated() → false (아직 작업 처리 중)
모든 작업 완료 후:
isShutdown() → true
isTerminated() → true
| newFixedThreadPool | newCachedThreadPool | newSingleThreadExecutor | |
|---|---|---|---|
| 코어 수 | nThreads | 0 | 1 |
| 최대 수 | nThreads | Integer.MAX_VALUE | 1 |
| 초기 수 | 0 | 0 | 0 |
| idle 소멸 | 없음 | 60초 후 | 없음 |
| 큐 | 무제한(LinkedBlocking) | 없음(SynchronousQueue) | 무제한(LinkedBlocking) |
| 적합한 상황 | 작업량 예측 가능 | 짧고 가벼운 작업 다수 | 순서 보장 필요 |
| 위험성 | 큐 무한 증가 | 스레드 폭증 | 처리량 제한 |