이 글은 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 를 한다.