RejectedExecutionException으로 인해 프로그램이 종료 되지 않은 경우김영한님의 강의를 듣던 중 스레드 풀이 가득차서 RejectedExecutionException이 발생할 때에 메인 스레드가 종료가 되지 않은 문제가 발생하였습니다.
아래 코드는 스레드 풀이 가득 찬 상태에서 작업을 의도적으로 추가해 거절(rejection)을 유발하는 예제입니다. task7 작업을 제출할 때 스레드 풀의 큐가 꽉 차거나 사용 가능한 스레드가 없을 경우, 작업이 거절되고 이후 RejectedExecutionException이 발생합니다.
처음 생각으로는 각 스레드에서 발생한 예외이기에 메인 스레드하고는 상관이 없다고 생각하였습니다.
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.RejectedExecutionException;
public class TestMain {
public static void main(String[] args) {
ArrayBlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);
ThreadPoolExecutor es = new ThreadPoolExecutor(2, 4, 3000, TimeUnit.MILLISECONDS, workQueue);
printState(es);
for (int i = 1; i <= 7; i++) {
es.execute(new RunnableTask("task" + i));
}
System.out.println("main - finish?");
printState(es);
es.shutdown();
System.out.println("프로그램 종료.");
}
private static void printState(ThreadPoolExecutor es) {
System.out.printf("Pool size: %d, Active threads: %d, Completed tasks: %d, Total tasks: %d%n",
es.getPoolSize(), es.getActiveCount(), es.getCompletedTaskCount(), es.getTaskCount());
}
static class RunnableTask implements Runnable {
private final String taskName;
public RunnableTask(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
System.out.println(taskName + " 실행 중");
try {
Thread.sleep(1000); // 간단한 작업 시뮬레이션
} catch (InterruptedException e) {
System.out.println(taskName + "에서 인터럽트 발생");
}
System.out.println(taskName + " 완료");
}
}
}
위 코드를 실행하면 task7 작업이 제출되는 시점에서 예외가 발생하게 됩니다. 그런데, 예외가 발생한 이후 프로그램이 종료되지 않는 문제를 경험할 수 있습니다.
shutdown()을 호출하고 있는데 왜 종료되지 않을까?// task7 제출 (예외 발생 가능)
es.execute(new RunnableTask("task7"));
System.out.println("main - finish?");
printState(es);
es.shutdown();
System.out.println("프로그램 종료.");
task7 작업을 제출하면서 예외가 발생했기 때문에 이후 코드가 실행되지 않고 프로그램이 종료되지 않습니다. 문제의 원인은 execute() 메서드가 작업을 스레드 풀에 전달하는 과정에서 발생하는 예외가 메인 스레드에서 발생하기 때문입니다. 이를 이해하기 위해 execute() 메서드를 살펴보겠습니다.
execute() 메서드 내부아래는 ThreadPoolExecutor의 execute() 메서드의 내부 코드입니다.

작업이 거절될 경우 reject() 메서드가 호출됩니다. 이를 통해 거절된 작업이 어떻게 처리되는지 확인해보겠습니다.
reject() 메서드reject() 메서드는 다음과 같이 RejectedExecutionHandler를 통해 거절된 작업을 처리합니다.

기본적으로 ThreadPoolExecutor는 AbortPolicy를 사용합니다. 이를 확인해보겠습니다.
AbortPolicy)AbortPolicy는 거절된 작업에 대해 RejectedExecutionException을 발생시킵니다.

따라서, task7 작업을 제출할 때 큐가 가득 차고 스레드도 여유가 없으면 RejectedExecutionException이 발생합니다. 이 예외는 메인 스레드에서 발생하기 때문에, 이후의 코드(shutdown() 호출 등)가 실행되지 않고 프로그램이 종료되지 않습니다.
문제를 해결하기 위해서는 RejectedExecutionException을 적절히 처리해야 합니다. 가장 간단한 방법은 try-catch 블록으로 예외를 처리하는 것입니다.
try {
es.execute(new RunnableTask("task7"));
} catch (RejectedExecutionException e) {
System.out.println("RejectedExecutionException 발생");
}
RejectedExecutionHandler 등록ThreadPoolExecutor에 사용자 정의 RejectedExecutionHandler를 등록하여 거절된 작업을 처리할 수도 있습니다.
package practice;
import java.util.concurrent.*;
public class TaskMain {
public static void main(String[] args) {
ArrayBlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);
ThreadPoolExecutor es = new ThreadPoolExecutor(
2, 4, 3000, TimeUnit.MILLISECONDS, workQueue,
new CustomRejectedExecutionHandler()
);
for (int i = 1; i <= 7; i++) {
es.execute(new RunnableTask("task" + i));
}
es.shutdown();
try {
es.awaitTermination(5000,TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("프로그램 종료.");
}
static class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
RunnableTask task = (RunnableTask)r;
System.out.println("작업 거절됨: " + task.taskName);
}
}
static class RunnableTask implements Runnable {
private final String taskName;
public RunnableTask(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
System.out.println(taskName + " 실행 중");
try {
Thread.sleep(1000); // 간단한 작업 시뮬레이션
} catch (InterruptedException e) {
System.out.println(taskName + "에서 인터럽트 발생");
}
System.out.println(taskName + " 완료");
}
}
}
RejectedExecutionException은 메인 스레드에서 발생하며, 이를 처리하지 않으면 프로그램의 흐름이 중단됩니다.try-catch로 처리하거나, 사용자 정의 RejectedExecutionHandler를 등록하여 거절된 작업을 처리할 수 있습니다.