스레드 관리를 쉽게 하기 위해서 사용하는 프레임워크다. 직접 스레드를 생성하기 보다는 Executor 프레임워크를 통해 스레드를 생성하는 경우가 많다.
정확히는 Executor가 아닌, 이를 구현한 ExecuotrService를 사용한다.
import java.util.concurrent.*; // Executor 관련 클래스들을 한 번에 임포트
import java.util.ArrayList;
import java.util.List;
public class ExecutorFrameworkExample {
public static void main(String[] args) throws InterruptedException {
System.out.println("--- Executor 프레임워크 예제 시작 ---\n");
// 1. ExecutorService 생성 (Executors 팩토리 메서드 사용)
// 4개의 스레드를 가진 고정된 스레드 풀을 생성합니다.
// 실무에서는 ThreadPoolExecutor를 직접 생성하는 것이 더 좋습니다.
ExecutorService executorService = Executors.newFixedThreadPool(4);
System.out.println("ExecutorService (고정 스레드 풀) 생성됨.");
// 스레드 풀의 현재 상태를 출력하는 헬퍼 메서드
printThreadPoolState(executorService);
// --- 2. Runnable 작업 제출 (값을 반환하지 않는 작업) ---
// Runnable은 run() 메서드를 가짐
Runnable task1 = () -> {
try {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + ": Task 1 시작 (Runnable)");
TimeUnit.SECONDS.sleep(2); // 2초 대기
System.out.println(threadName + ": Task 1 완료 (Runnable)");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println(Thread.currentThread().getName() + ": Task 1 인터럽트됨.");
}
};
Runnable task2 = () -> {
try {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + ": Task 2 시작 (Runnable)");
TimeUnit.SECONDS.sleep(1); // 1초 대기
System.out.println(threadName + ": Task 2 완료 (Runnable)");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println(Thread.currentThread().getName() + ": Task 2 인터럽트됨.");
}
};
System.out.println("\nRunnable 작업 제출:");
executorService.execute(task1); // execute는 Future를 반환하지 않음
Future<?> futureOfTask2 = executorService.submit(task2); // submit은 Future를 반환함
// Future를 사용하여 Runnable 작업의 완료 여부 확인
try {
System.out.println("Task 2 완료 여부 확인 중...");
// futureOfTask2.get(); // Runnable의 get()은 null을 반환하며 작업 완료까지 블로킹
while (!futureOfTask2.isDone()) {
System.out.print(".");
TimeUnit.MILLISECONDS.sleep(100);
}
System.out.println("\nTask 2가 완료되었습니다.");
} catch (Exception e) {
System.err.println("Task 2 처리 중 오류 발생: " + e.getMessage());
}
// 스레드 풀의 현재 상태 출력
printThreadPoolState(executorService);
// --- 3. Callable 작업 제출 (값을 반환하는 작업) ---
// Callable은 call() 메서드를 가짐 (Checked Exception 던질 수 있음)
Callable<String> callableTask1 = () -> {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + ": Callable Task 1 시작 (결과 반환)");
TimeUnit.SECONDS.sleep(3); // 3초 대기
return threadName + "로부터의 결과: 계산 완료!";
};
Callable<Integer> callableTask2 = () -> {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + ": Callable Task 2 시작 (정수 반환)");
int sum = 0;
for (int i = 0; i < 1000000; i++) {
sum += i; // 간단한 계산
}
TimeUnit.MILLISECONDS.sleep(500); // 0.5초 대기
return sum;
};
System.out.println("\nCallable 작업 제출:");
List<Future<?>> futures = new ArrayList<>();
futures.add(executorService.submit(callableTask1));
futures.add(executorService.submit(callableTask2));
// Future를 사용하여 Callable 작업의 결과 가져오기
for (Future<?> future : futures) {
try {
// get()은 작업이 완료될 때까지 블로킹합니다.
System.out.println("작업 결과: " + future.get());
} catch (InterruptedException | ExecutionException e) {
System.err.println("작업 결과 가져오기 오류: " + e.getMessage());
}
}
// 스레드 풀의 현재 상태 출력
printThreadPoolState(executorService);
// --- 4. 스레드 풀 종료 ---
// 더 이상 새 작업을 받지 않고, 현재 큐에 있는 모든 작업이 완료될 때까지 기다림.
System.out.println("\n스레드 풀 종료 요청...");
executorService.shutdown();
// 모든 작업이 완료될 때까지 최대 10초 대기
try {
if (executorService.awaitTermination(10, TimeUnit.SECONDS)) {
System.out.println("모든 작업이 성공적으로 종료되었습니다.");
} else {
System.out.println("일부 작업이 시간 내에 종료되지 않았습니다.");
// 시간이 초과되었을 경우 강제 종료를 시도할 수 있습니다.
// executorService.shutdownNow();
}
} catch (InterruptedException e) {
System.err.println("종료 대기 중 인터럽트 발생.");
Thread.currentThread().interrupt();
}
System.out.println("\n--- Executor 프레임워크 예제 종료 ---");
}
/**
* ExecutorService의 현재 상태를 출력하는 헬퍼 메서드 (ThreadPoolExecutor인 경우 상세 정보)
* Java 14+의 instanceof 패턴 매칭 사용
*/
public static void printThreadPoolState(ExecutorService executorService) {
if (executorService instanceof ThreadPoolExecutor poolExecutor) {
int poolSize = poolExecutor.getPoolSize();
int activeThreads = poolExecutor.getActiveCount();
int queuedTasks = poolExecutor.getQueue().size();
long completedTasks = poolExecutor.getCompletedTaskCount();
long totalTasks = poolExecutor.getTaskCount();
System.out.println(
String.format("[PoolState] Pool: %d, Active: %d, Queued: %d, Completed: %d, Total: %d",
poolSize, activeThreads, queuedTasks, completedTasks, totalTasks)
);
} else {
System.out.println("[PoolState] Generic ExecutorService (상세 정보 없음)");
}
}
}
이처럼 Executors.newFixedThreadPool(스레드 갯수)로 만들 수도 있고, ExecutorService es = new ThreadPoolExecutor(corePoolSize,maximumPoolSize, keepAliveTime,
TimeUnit.Unit, BlockingQueue workQueue<>()) 이런 형식으로도 만들 수 있다.
Executor를 사용하면 스레드 관리 어려움을 극복할 수 있다.
기존에는 스레드를 직접 생성해야 했기 때문에 갑작스럽게 늘어나는 스레드 수에 대응하기 어려웠고, 중간에 스레드에 인터럽트를 거는 데에 불편함이 있었다.
게다가 Runnable의 run() 메서드는 void라 값을 직접 반환할 수 없었고, 예외도 던질 수 없었다.
그래서 이러한 문제를 해결하기 위한 것이 Executor 프레임워크다.
간단히 말해서 스레드를 모아놓은 풀을 따로 만들어서 그 안에 스레들 모아 관리하고, 작업이 생기면 스레드를 보내 작업을 처리시킨다. 처리가 끝나면 스레드를 종료하는 것이 아니라 다시 스레드에 돌아와 반납된다.
Execuotr를 닫는 메서드.
스레드가 종료되지 않고 반납되므로 작업이 돤료되면 닫아줄 필요가 있다.