스레드 풀 생성(ThreadPoolExecutor)
스레드 풀을 자바에서 구현한 구현체가 ThreadPoolExecutor이다.
newCachedThreadPool()
newFixedThreadPool(int nThreads)
코어수는 스레드 제거시 최소 몇개의 스레드를 남겨 놓을지를 의미, 코어 수가 3이면 최소 3개는 남겨놓음.
| 메소드명(매개변수) | 초기 수 | 코어 수 | 최대 수 |
|---|---|---|---|
| newCachedThreadPool() | 0 | 0 | Integer.MAX_VALUE |
| newFixedThreadPool(int nThreads) | 0 | 생성된 수 | nThreads |
ExecutorService threadPool = new ThreadPoolExecutor(
3, // 코어 스레드 개수
100, // 최대 스레드 개수
120L, // 놀고 있는 시간
TimeUnit.SECONDS, // 놀고 있는 시간 단위
new SynchronousQueue<Runnable>() // 작업 큐
);

new SynchronousQueue() → 이 동기 기능으로 인해 작업 큐에 한번에 한 스레드만 작업 처리 요청을 보내게됨. 동시에 보내면 문제 됨.
스레드 풀 종료
스레드풀의 스레드는 main 스레드가 종료되더라도 작업을 처리하기 위해 계속 실행 상태로 남아있음.
스레드풀의 모든 스레드를 종료하려면 ExecutorService의 다음 두 메소드 중 하나를 실행 해야함.
| 리턴 타입 | 메소드명(매개변수) | 설명 |
|---|---|---|
| void | shutdown() | 현재 처리 중인 작업뿐만 아니라 작업 큐에 대기하고 있는 모든 작업을 처리한 뒤에 스레드풀을 종료시킨다. |
| List | shutdonwNow() | 현재 작업 처리 중인 스레드를 interrupt해서 (작업이 완료되지 않았더라도) 작업을 중지시키고 스레드풀을 종료시킨다. 리턴값은 작업 큐에 있는 미처리된 작업(Runnable)의 목록(list)이다. |
shutdownNow()는 실행중이던 작업이 완료되지 않았어도 중지시키기에 손실이 발생할 수 있음.
shutdown()이 안전함.
Runnable은 thread가 처리해야 할 작업 내용을 가지고 있는 객체
하나의 작업은 Runnable 또는 Callable 구현 클래스로 표현 함.
둘의 차이는 작업 처리 완료 후 리턴값이 있냐 없냐와 예외를 던지냐 안던지냐의 차이.
new Runnable() {
@Override
public void run() {
// 스레드가 처리할 작업 내용
}
}
new Callable<T> {
@Override
public T call() throws Exception {
// 스레드가 처리할 작업 내용
return T;
}
}
public class RunnableExecuteExample {
public static void main(String[] args) {
// 1000개의 메일(스레드) 생성
String[][] mails = new String[1000][3];
for (int i = 0; i < mails.length; i++) {
mails[i][0] = "admin@my.com";
mails[i][1] = "member"+i+"@my.com";
mails[i][2] = "신상";
}
// ExecutorService 생성
ExecutorService executorService = Executors.newFixedThreadPool(5); // 최대 스레드 수 5개로 제한
// 이메일을 보내는 작업 생성 및 처리 요청
for (int i = 0; i < 1000; i++) {
final int idx = i;
executorService.execute(new Runnable() {
@Override
public void run() {
Thread thread = Thread.currentThread(); // 현재 실행하는 Thread에 대한 참조 (실행되고 있는 Thread중 어떤 Thread가 이 작업을 처리하고 있는지)
String from = mails[idx][0];
String to = mails[idx][1];
String content = mails[idx][2];
System.out.println("[" + thread.getName() + "]" + from + " ==> " + to " +: " + content);
}
});
}
// ExecutorService 종료
executorService.shutdown();
}
| 리턴 타입 | 메소드명(매개변수) | 설명 |
|---|---|---|
| void | execute(Runnable command) | - Runnable을 작업 큐에 저장 |
| - 작업 처리 결과를 리턴하지 않음 | ||
| Future | submit(Callable task) | - Callable을 작업 큐에 저장 |
| - 작업 처리 결과를 얻을 수 있도록 Future를 리턴 |
public class CallableSubmitExample {
public static void main(String[] args) {
// ExecutorService 생성
ExecutorService executorService = Executors.newFixedThreadPool(5);
// 계산 작업 생성 및 처리 요청
for (int i = 1; i <= 100; i++) {
final int idx = i;
Future<Integer> future = executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i=1; i<=idx; i++) {
sum += i;
}
Thread thread = Thread.currentThread();
return sum;
}
});
try {
int result = future.get(); // call 메서드의 리턴값이 생기기전까지 대기함.
} catch (Exception e) {
e.printStackTrace();
}
}
executorService.shutdown();
}
}
스레드 풀을 사용하지 않고 요청마다 스레드를 생성하면, 스레드 생성에 소요되는 시간 때문에 요청처리가 더 오래 걸림.
처리 속도보다 더 빠르게 요청이 늘어나면
스레드 풀 사용시 주의할 점
작업 큐에 제한이 있는 지 확인.
작업 큐에 제한이 없다면, 만약 모든 스레드가 요청을 처리 중이어서 작업큐에 요청이 무한정 쌓이게 된다면 메모리를 고갈시키게 되고 전체 시스템에 문제를 발생시키게 됨.
이 경우에는 작업 큐에 제한을 두어 버려지는 요청이 생기더라도 시스템에 문제가 발생하지 않도록 해야함.
public class ThreadPoolWithLimitedQueue {
public static void main(String[] args) {
// 제한된 크기의 작업 큐 (최대 5개까지 대기 가능)
BlockingQueue<Runnable> taskQueue = new ArrayBlockingQueue<>(5);
// 스레드 풀 생성 (코어 스레드 2개, 최대 4개, 작업 큐 taskQueue 사용)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 코어 스레드 개수
4, // 최대 스레드 개수
60, TimeUnit.SECONDS, // 초과 스레드의 유휴 시간
taskQueue, // 작업을 저장할 큐 (크기 제한)
new ThreadPoolExecutor.AbortPolicy() // 큐가 꽉 차면 예외 발생
);
// 10개의 작업을 제출
for (int i = 1; i <= 10; i++) {
final int taskId = i;
try {
executor.execute(() -> {
System.out.println("작업 " + taskId + " 실행 (thread: " + thread.currentThread().getName() + ")");
try {
Thread.sleep(2000); // 작업 수행 시간
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
} catch (RejectedExecutionException e) {
System.out.println("작업 " + taskId + "이(가) 큐가 꽉 차서 거부됨");
}
}
executor.shutdown();
}
}
작업 8 실행 (Thread: pool-3-thread-3)
작업 2 실행 (Thread: pool-3-thread-2)
작업 9 실행 (Thread: pool-3-thread-4)
작업 10이(가) 큐가 꽉 차서 거부됨!
작업 1 실행 (Thread: pool-3-thread-1)
작업 3 실행 (Thread: pool-3-thread-4)
작업 5 실행 (Thread: pool-3-thread-2)
작업 4 실행 (Thread: pool-3-thread-3)
작업 6 실행 (Thread: pool-3-thread-1)
작업 7 실행 (Thread: pool-3-thread-4)
최대 스레드 수를 5로 늘리면 거부 메시지가 뜨지 않음.