개발/자바
Thread ExecutorService API
피터JK
2025. 2. 17. 14:48
728x90
ExecutorService 설명
ExecutorService는 Java의 스레드 풀(Thread Pool) 을 제공하는 인터페이스로, 직접 Thread 객체를 생성하는 대신 효율적으로 스레드를 관리할 수 있게 해줍니다.
1. ExecutorService의 개념
- 스레드 풀(Thread Pool) 을 관리하는 고급 API
- 스레드를 미리 생성하여 재사용하는 방식
- execute()와 submit()을 통해 작업을 실행 가능
- 스레드 생성 비용 절감 및 성능 향상
- Deadlock 방지, 스레드 개수 제한 등의 효과
🔹 사용하지 않으면?
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Task());
thread.start();
}
이 방식은 매번 새로운 Thread를 생성하여 비효율적이고, 많은 리소스를 소비함.
🔹 ExecutorService 사용하면?
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
executor.submit(new Task());
}
executor.shutdown();
👉 스레드를 재사용하여 성능 최적화 가능!
2. 주요 메서드
| 메서드 | 설명 |
| execute(Runnable command) | 작업 실행 (반환값 없음) |
| submit(Callable/Vunnable task) | 작업 실행 후 Future 반환 |
| shutdown() | 기존 작업 수행 후 종료 |
| shutdownNow() | 즉시 종료 (대기 중인 작업 취소) |
| isShutdown() | shutdown() 호출 여부 확인 |
| isTerminated() | 모든 작업 종료 여부 확인 |
| invokeAll(Collection tasks) | 모든 작업을 실행하고, 결과 리스트 반환 |
| invokeAny(Collection tasks) | 가장 먼저 완료된 작업의 결과 반환 |
3. 다양한 스레드 풀 생성 방법
Executors 클래스를 사용하여 다양한 방식의 스레드 풀을 생성할 수 있음.
(1) 고정 크기 스레드 풀 (FixedThreadPool)
ExecutorService executor = Executors.newFixedThreadPool(3);
- 3개의 스레드만 사용
- 제한된 개수의 작업을 처리하는 경우 적합
- 과부하 방지에 유용
(2) 캐시 스레드 풀 (CachedThreadPool)
ExecutorService executor = Executors.newCachedThreadPool();
- 필요한 만큼 동적으로 스레드 생성
- 유휴(Idle) 상태가 되면 스레드 자동 삭제
- 작업량이 변동하는 경우 적합
- 단점: 너무 많은 스레드가 생성될 가능성이 있음
(3) 단일 스레드 풀 (SingleThreadExecutor)
ExecutorService executor = Executors.newSingleThreadExecutor();
- 하나의 스레드로 작업을 순차 실행
- 작업 순서를 유지해야 하는 경우 적합
- 예: 로그 처리, 파일 I/O 등
(4) 일정 시간 간격으로 실행되는 스레드 풀 (ScheduledThreadPool)
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
executor.schedule(() -> System.out.println("3초 후 실행!"), 3, TimeUnit.SECONDS);
- 특정 시간 후 실행 or 주기적 실행 가능
- 스케줄링 작업에 적합
4. 실행 예제
(1) FixedThreadPool 예제
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class Task implements Runnable {
public void run() {
System.out.println(Thread.currentThread().getName() + " 작업 실행");
}
}
public class FixedThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
executor.submit(new Task());
}
executor.shutdown();
}
}
출력 예시:
pool-1-thread-1 작업 실행
pool-1-thread-2 작업 실행
pool-1-thread-3 작업 실행
pool-1-thread-1 작업 실행
pool-1-thread-2 작업 실행
설명:
- 3개의 스레드가 5개의 작업을 나눠서 처리
- 스레드 재사용
(2) Callable & Future 사용 (값 반환)
Runnable은 반환값이 없지만, Callable은 값을 반환할 수 있음.
import java.util.concurrent.*;
class MyCallable implements Callable<String> {
public String call() {
return "작업 완료!";
}
}
public class CallableExample {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(new MyCallable());
System.out.println(future.get()); // 블로킹 호출
executor.shutdown();
}
}
출력:
작업 완료!
- Future.get()을 호출하면 결과가 나올 때까지 블로킹됨
5. shutdown() vs shutdownNow()
🔹 shutdown()
- 기존 작업을 모두 수행한 후 종료
- 추가 작업 요청은 거부됨
executor.shutdown();
🔹 shutdownNow()
- 즉시 실행 중인 작업을 중단 시도
- 대기 중인 작업은 취소됨
executor.shutdownNow();
6. invokeAll()과 invokeAny()
(1) invokeAll()
- 여러 Callable을 실행하고, 모든 작업이 완료될 때까지 대기
- 결과를 List<Future>로 반환
import java.util.concurrent.*;
public class InvokeAllExample {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(3);
Callable<String> task1 = () -> "Task 1 완료";
Callable<String> task2 = () -> "Task 2 완료";
Callable<String> task3 = () -> "Task 3 완료";
var futures = executor.invokeAll(List.of(task1, task2, task3));
for (Future<String> future : futures) {
System.out.println(future.get());
}
executor.shutdown();
}
}
출력:
Task 1 완료
Task 2 완료
Task 3 완료
(2) invokeAny()
- 여러 Callable 중 가장 먼저 끝난 작업의 결과만 반환
- 나머지 작업은 취소됨
import java.util.concurrent.*;
public class InvokeAnyExample {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(3);
Callable<String> task1 = () -> {
Thread.sleep(2000);
return "Task 1 완료";
};
Callable<String> task2 = () -> {
Thread.sleep(1000);
return "Task 2 완료";
};
Callable<String> task3 = () -> {
Thread.sleep(3000);
return "Task 3 완료";
};
String result = executor.invokeAny(List.of(task1, task2, task3));
System.out.println("가장 빨리 끝난 작업: " + result);
executor.shutdown();
}
}
출력:
가장 빨리 끝난 작업: Task 2 완료
👉 Task 2가 가장 먼저 끝났기 때문에 나머지는 취소됨.
7. 결론
| 방식 | 특징 |
| FixedThreadPool | 고정된 개수의 스레드 유지 |
| CachedThreadPool | 필요할 때마다 스레드 생성 (최적화) |
| SingleThreadExecutor | 단일 스레드로 순차적 실행 |
| ScheduledThreadPool | 일정 간격/지연 시간 후 실행 |
| invokeAll() | 모든 작업 완료 후 결과 리스트 반환 |
| invokeAny() | 가장 먼저 끝난 작업 결과만 반환 |
💡 ExecutorService를 활용하면 멀티스레드 관리를 쉽게 할 수 있습니다!
728x90