[Java] ExecutorService.shutdown(Now) & awaitTermination

식빵·2023년 1월 18일
0

Java Lab

목록 보기
9/21
post-thumbnail

😒 개요

이 글은 ExecutorService 가 제공하는 3가지 API ...

  • void shutdown();
  • List<Runnable> shutdownNow();
  • boolean awaitTermination(long timeout, TimeUnit unit);

... 에 대한 공부 내용을 기록하는 글이다.


API 만 따로 조사하는 이유는 이 3가지가 조합되서 사용되는 경우가 많고
이 과정에서 많은 혼동을 일으키기 때문이다.


해당 메소드들에 대한 상세한 내용은 이미 javaDoc 이 있다.
하지만 이 중에서 핵심만 잘 뽑아낸 StackOverflow 글이 있어서 아래에 첨부했다.

출처: https://stackoverflow.com/questions/18425026/shutdown-and-awaittermination-which-first-call-have-any-difference



가볍게 해석만 하면 물론 이해가 될 수도 있지만,
위 글을 내 방식대로 재해석하고 관련된 테스트를 해봤다.
그리고 추가적으로 조사했던 내용들도 덧붙여서 아래에 작성해봤다.



🍀 shutdownNow


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초 후에 종료를 시도하지만, 멈추지 않는 것을 확인할 수 있다.





🍀 shutdown


  • ExecutorService.shutdown() 은 ThreadPool 에 요청되는 submit 을 더 이상
    받아주지 않는 대신, 기존에 실행 중이던 쓰레드 풀의 쓰레드들은 계속해서 실행을 한다.

  • shutdown 후 executorService.submit 을 하면 RejectedExecutionException 예외가 터진다.

  • shutdownNow 처럼 즉시 멈추려는 의도가 아닌, 즉 graceful 하게 끝내려는 느낌이 강한 메소드이다.

  • 주의할 점은 InterruptedException 를 통해서만 멈추는 작업이 submit 되어있다면, shutdown 을 통해서 종료될 수 없다. 그때는 shutdownNow 를 호출해야 한다.



⚠️ shutdown is non-blocking

주의할 게 있다.
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 메소드에 대해서도 같은 동작을 보인다.





🍀 awaitTermination


하지만 가끔은 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.awaitTerminationboolean 반환값

이 반환값은 의미는 awaitTermination 는 인자로 timeout 값과 연관이 있다.

  • 만약 timeout 으로 주어진 시간 내에 shutdown 이 끝나지 않으면 false 를 반환
  • 반대로 주어진 시간 내에 shutdown 이 완료도면 true 를 반환
  • 이외에도 executorService.shutdown 호출 쓰레드 (여기서는 main 쓰레드) 가 interrupt 상태이면 그냥 InterruptedException 를 던진다.





🍀 추가조사


🥝 제대로 shutdown 시키려면?

위의 내용을 모두 이해하면 아래 코드가 이해가 될 것이다.
참고로 아래 코드는 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 되었을 때 즉시 처리될 수 있습니다.



🥝 명시적 shutdown 호출

ExecutorService 가 생성하는 쓰레드 풀은 내부적으로
ThreadFactory.defaultThreadFactory 를 기본으로 사용한다.

그리고 이 ThreadFactory.defaultThreadFactory 가 생성하는 쓰레드는
기본적으로 non-daemon 쓰레드이다. 즉 명시적으로 우리가 종료하지 않으면
프로세스가 계속 살아있을 수 있다는 것이다. (StackOverflow 참고)


ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(() -> {
	System.out.println("wow");
});

위처럼 코딩하고 실행하면 프로세스가 종료되지 않고 계속 실행상태인 것을 확인할 수 있다.
이러는 이유는 ExecutorsService 는 단 한번이라도 submit 을 하면 그때 쓰레드 풀에 Worker 쓰레드가 생겨서 그렇다.

이런 이유로 ExecutorServicesubmit 을 호출한 이후 종료를 시키고 싶으면
"명시적"으로 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 의 shutdown 방식

java 19 버전 이후 ExecutorService 들이 모두 autocloseable interface
구현하므로 아래처럼 shutdown 시킬 수 있다.

try (ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor()) {   
    singleThreadExecutor.submit(() -> "running inside a try");
}

try-catch 내의 모든 executorService 를 통한 작업이 끝나면 자동으로 close 를 한다.





✨ 참고 링크


profile
백엔드를 계속 배우고 있는 개발자입니다 😊

0개의 댓글