
이 글은 ExecutorService 가 제공하는 3가지 API ...
void shutdown();List<Runnable> shutdownNow();boolean awaitTermination(long timeout, TimeUnit unit);... 에 대한 공부 내용을 기록하는 글이다.
이 API 만 따로 조사하는 이유는 이 3가지가 조합되서 사용되는 경우가 많고
이 과정에서 많은 혼동을 일으키기 때문이다.
해당 메소드들에 대한 상세한 내용은 이미 javaDoc 이 있다.
하지만 이 중에서 핵심만 잘 뽑아낸 StackOverflow 글이 있어서 아래에 첨부했다.
가볍게 해석만 하면 물론 이해가 될 수도 있지만,
위 글을 내 방식대로 재해석하고 관련된 테스트를 해봤다.
그리고 추가적으로 조사했던 내용들도 덧붙여서 아래에 작성해봤다.
ExecutorService.shutdownNow() 는 쓰레드 풀의 모든 쓰레드에
thread.interrupt()를 실행시켜서 하던 작업을 모두 멈추게 하는 것이다.
주의!!!
ExecutorService에 실행 중인Runnable(또는 Callable)의 구현이
InterruptedException예외를 catch 하여 동작을 멈추는 처리가 없거나- interrupt flag 를 검사하는 코드가 없으면...
shutdownNow하더라도 프로그램이 종료되지 않을 수 있다.
이해가 안된다면 아래예제 1코드를 확인해보자.
예제 1코드:public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newCachedThreadPool(); executorService.submit(() -> { while(true) { try { Thread.sleep(1000L); System.out.println("아무도 날 막지 못해!!!"); } catch (InterruptedException e) { // 절대로 끝나지 않음! System.out.println("ignore InterruptedException!!!"); // 끝나게 하려면 break 이든 return 이든 뭔가를 해줘야함 } } }); // 3초 후에 Shutdown 시도 Thread.sleep(3000); executorService.shutdownNow(); }
예제 1실행 결과:![]()
- 3초 후에 종료를 시도하지만, 멈추지 않는 것을 확인할 수 있다.
ExecutorService.shutdown() 은 ThreadPool 에 요청되는 submit 을 더 이상
받아주지 않는 대신, 기존에 실행 중이던 쓰레드 풀의 쓰레드들은 계속해서 실행을 한다.
shutdown 후 executorService.submit 을 하면 RejectedExecutionException 예외가 터진다.
shutdownNow 처럼 즉시 멈추려는 의도가 아닌, 즉 graceful 하게 끝내려는 느낌이 강한 메소드이다.
주의할 점은 InterruptedException 를 통해서만 멈추는 작업이 submit 되어있다면, shutdown 을 통해서 종료될 수 없다. 그때는 shutdownNow 를 호출해야 한다.
주의할 게 있다.
shutdown 메소드를 호출하면, 해당 호출한 line 에서 blocking이 일어나지 않는다.
더 자세히 설명하자면...
ExecutorService.shutdown 을 "호출한 쓰레드"에서 Blocking 을
발생하지 않는다. 즉 main 쓰레드에서 ExecutorService.shutdown 를 호출하면
해당 호출문에서 block 이 되지 않고 바로 다음으로 넘어가서 main 쓰레드는 종료된다.
아래처럼 코드를 작성해보고 실행시켜 보자.
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(() -> {
try {
Thread.sleep(1000L);
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
return;
}
});
executorService.shutdown();
System.out.println("main thread 끝!");
}
실행하면 아래와 같이 나온다.
main thread 끝!
pool-1-thread-1
보면 알겠지만, main 쓰레드에서 executorService.shutdown(); 다음에 있는
System.out.println("main thread 끝!"); 가 먼저 실행되는 것을 확인할 수 있다.
즉 executorService.shutdown(); 는 blocking 이 발생하지 않는 것이다.
참고로 shutdownNow 메소드에 대해서도 같은 동작을 보인다.
하지만 가끔은 shutdown(또는 shutdownNow) 을 호출한 쓰레드에서 blocking 이 필요할 수도 있다. 이때 사용 가능한 게 awaitTermination 메소드이다.
간단하게 ExecutorService 에 submit 을 하고, shutdown 을 호출한 후,
몇초가 걸려서 완전히 shutdown 이 되는지 확인하는 코드 예시를 아래처럼 작성해봤다.
코드를 보면서 이해해보자.
package me.dailycode.reactive._01;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class JavaCompletableFuture {
public static void main(String[] args) throws InterruptedException {
// awaitTermination 를 이용해서 시간측정을 하려고 한다.
// 일단 시작 시간을 저장한다.
LocalDateTime startTime = LocalDateTime.now();
// 쓰레드풀 생성
ExecutorService executorService = Executors.newFixedThreadPool(4);
// ? ~ 5 초 사이에 끝나는 Runnable 들을 submit 한다.
executorService.submit(getRunnable(new Random().nextLong(3000,5000)));
executorService.submit(getRunnable(new Random().nextLong(2000,5000)));
executorService.submit(getRunnable(new Random().nextLong(4000,5000)));
executorService.submit(getRunnable(new Random().nextLong(3000,5000)));
// ExecutorService shutdown!
executorService.shutdown();
// executorService.shutdown(); 를 호출한 main 쓰레드가
// executorService 가 완전히 종료될 때까지 기다리는(= blocking)
// 하는 작업을 수행한다. 기다리는 시간을 첫번째 파라미터로 넣는데,
// 여기서는 무한정 대기인 Long.MAX_VALUE 을 준다.
System.out.println("Blocking 시작!");
executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
// blocking 하는 데 걸린 시간 측정
LocalDateTime endTime = LocalDateTime.now();
System.out.println("걸린 시간: "
+ Duration.between(startTime, endTime).toSeconds() + "초");
System.out.println("main thread 끝!");
}
private static Runnable getRunnable(Long time) {
return () -> {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
System.out.println("InterruptedException occurred from ... "
+ Thread.currentThread().getName());
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName());
};
}
}
참고:
executorService.awaitTermination의boolean반환값이 반환값은 의미는
awaitTermination는 인자로timeout값과 연관이 있다.
- 만약 timeout 으로 주어진 시간 내에 shutdown 이 끝나지 않으면
false를 반환- 반대로 주어진 시간 내에 shutdown 이 완료도면
true를 반환- 이외에도 executorService.shutdown 호출 쓰레드 (여기서는
main쓰레드) 가interrupt상태이면 그냥InterruptedException를 던진다.
위의 내용을 모두 이해하면 아래 코드가 이해가 될 것이다.
참고로 아래 코드는 ExecutorService javaDoc 에 작성된 코드 예시이다.
void shutdownAndAwaitTermination(ExecutorService pool) {
pool.shutdown(); // Disable new tasks from being submitted
try {
// Wait a while for existing tasks to terminate
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
pool.shutdownNow(); // Cancel currently executing tasks
// Wait a while for tasks to respond to being cancelled
if (!pool.awaitTermination(60, TimeUnit.SECONDS))
System.err.println("Pool did not terminate");
}
} catch (InterruptedException ie) {
// (Re-)Cancel if current thread also interrupted
pool.shutdownNow();
// Preserve interrupt status
Thread.currentThread().interrupt();
}
}
코드 해설 (feat.ChatGPT)
위 코드는 ExecutorService 인스턴스를 graceful 하게 shutdown 하기 위한 코드입니다.
우선 pool.shutdown() 메소드는 새로운 작업이 제출되지 않도록 ExecutorService 를 종료시킵니다. 이후 pool.awaitTermination() 메소드는 모든 작업이 완료되는데 까지 최대 60초간 기다립니다.
만약 모든 작업이 완료되지 않은 상태에서 위 조건이 만족하지 않으면, pool.shutdownNow() 메소드를 호출하여 현재 실행 중인 모든 작업을 취소하고 실행되지 않은 작업 목록도 삭제합니다. 이후에 pool.awaitTermination() 메소드를 호출하여 모든 작업의 취소 및 삭제를 기다립니다. 이 과정에서 60초 이내에 작업이 완료되지 않으면 "Pool did not terminate" 메시지를 출력합니다.
만약 pool.awaitTermination() 메소드가 InterruptedException 예외를 던진다면, 이는 대기 중인 스레드가 interrupt 됐음을 나타냅니다. 이 경우, pool.shutdownNow() 메소드를 호출하여 현재 실행 중인 작업을 취소하고, Thread.currentThread().interrupt() 메소드를 호출하여 현재 스레드의 interrupt 상태를 복원합니다. 이렇게 하면 대기 중인 스레드가 interrupt 되었을 때 즉시 처리될 수 있습니다.
ExecutorService 가 생성하는 쓰레드 풀은 내부적으로
ThreadFactory.defaultThreadFactory 를 기본으로 사용한다.
그리고 이 ThreadFactory.defaultThreadFactory 가 생성하는 쓰레드는
기본적으로 non-daemon 쓰레드이다. 즉 명시적으로 우리가 종료하지 않으면
프로세스가 계속 살아있을 수 있다는 것이다. (StackOverflow 참고)
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(() -> {
System.out.println("wow");
});
위처럼 코딩하고 실행하면 프로세스가 종료되지 않고 계속 실행상태인 것을 확인할 수 있다.
이러는 이유는 ExecutorsService 는 단 한번이라도 submit 을 하면 그때 쓰레드 풀에 Worker 쓰레드가 생겨서 그렇다.
이런 이유로 ExecutorService 는 submit 을 호출한 이후 종료를 시키고 싶으면
"명시적"으로 shutdown (혹은 shutdownNow) 를 호출해야 한다.
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(() -> {
System.out.println("wow");
});
executorService.shutdown();
예외1)
Executors.newCachedThreadPool()는 사용되지 않는 쓰레드의 경우 60초 후에 삭제 시켜서shutdown을 호출하지 않아도 어느시점에는 프로세스가 종료된다.예외2)
ForKJoinPool.commonPool()의 경우에는daemon쓰레드로 구성되기 때문에
프로세스 종료에 영향을 끼치지 않는다.
java 19 버전 이후 ExecutorService 들이 모두 autocloseable interface 를
구현하므로 아래처럼 shutdown 시킬 수 있다.
try (ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor()) {
singleThreadExecutor.submit(() -> "running inside a try");
}
try-catch 내의 모든 executorService 를 통한 작업이 끝나면 자동으로 close 를 한다.