Java/기초

ExecutorService in Java: Complete Guide with Practical Examples - 1

blogger903 2024. 9. 2. 14:54
728x90

ExecutorService in Java: Complete Guide with Practical Examples

Java/Spring으로 여러 API를 비동기로 요청을 처리해야할때 내부적으로 ExecutorService를 사용합니다

이번 포스팅에서는 ExecutorService에 대해서 자세히 다뤄보겠습니다

 

환경

  • java17

다루는 내용

  • ExecutorService 개요
  • execute vs submit
  • 스레드풀 중단 및 종료
  • 작업 종료 대기 및 확인

 

ExecutorService 개요

ExecutorService는 비동기 작업을 실행하고 관리하기 위해 두 가지 메서드를 제공합니다

  • void execute(Runnable r): 작업을 제출하면 작업을 실행하고 종료합니다
  • Future submit(Callable c): 작업을 제출하면 작업을 실행함과 동시에 Future를 반환합니다. Future에 결과값을 포함하고 있습니다
    • Runnable도 인자로 받아서 실행할 수 있습니다

execute vs submit

execute는 반환값이 없고, submit은 결과값을 반환합니다

execute, submit 두 메서드로 작업을 실행하겠습니다

 

execute 예시 코드입니다

import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
  
//출력  
//> Task :ExecuteExample.main()  
//메인 스레드: main  
//ExecutorService 스레드: pool-1-thread-1  
//Runnable 작업 실행 (ExecutorService)//새로운 스레드: Thread-0  
//Runnable 작업 실행 (새로운 Thread)  
public class ExecuteExample {  
    public static void main(String[] args) {  
  
        System.out.println("메인 스레드: " + Thread.currentThread().getName());  
  
        ExecutorService executorService = Executors.newSingleThreadExecutor();  
  
        // Runnable을 사용한 작업 실행 (execute 메서드)  
        executorService.execute(() -> {  
            System.out.println("ExecutorService 스레드: " + Thread.currentThread().getName());  
            System.out.println("Runnable 작업 실행 (ExecutorService)");  
        });  
  
        // 스레드 풀 종료  
        executorService.shutdown();  
  
        new Thread(() -> {  
            System.out.println("새로운 스레드: " + Thread.currentThread().getName());  
            System.out.println("Runnable 작업 실행 (새로운 Thread)");  
        }).start();  
    }  
}

 

submit 메서드 예시입니다

 

  
import java.time.Instant;  
import java.time.format.DateTimeFormatter;  
import java.util.concurrent.ExecutionException;  
import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
import java.util.concurrent.Future;  
  
/**  
 * 출력  
 * > Task :SubmitCallableExample.main()  
 * 2024-09-01T23:54:48.823592Z|main|메인 메소드 시작  
 * 2024-09-01T23:54:48.834334Z|pool-1-thread-1|Callable 작업 실행  
 * 2024-09-01T23:54:48.836543Z|main|작업 결과: 42  
 * 2024-09-01T23:54:48.836968Z|Thread-0|Runnable 작업 실행  
 * 2024-09-01T23:54:49.840452Z|Thread-0|작업 결과: 42  
 * 2024-09-01T23:54:49.841497Z|main|=============================================== * 2024-09-01T23:54:49.841695Z|main|모든 작업 완료  
 */  
  
public class SubmitCallableExample {  
    private static final DateTimeFormatter ISO_FORMATTER = DateTimeFormatter.ISO_INSTANT;  
  
    private static String getCurrentTime() {  
        return Instant.now().toString();  
    }  
  
    private static void log(String message) {  
        System.out.println(getCurrentTime() + "|" + Thread.currentThread().getName() + "|" + message);  
    }  
  
    public static void main(String[] args) throws ExecutionException, InterruptedException {  
        log("메인 메소드 시작");  
  
        ExecutorService executorService = Executors.newSingleThreadExecutor();  
  
        // Callable을 사용한 작업 실행 (submit 메서드)  
        Future<Integer> future = executorService.submit(() -> {  
            log("Callable 작업 실행");  
            return 42;  
        });  
  
        // 작업 결과를 가져옴  
        int result = future.get();  
        log("작업 결과: " + result);  
  
        // 스레드 풀 종료  
        executorService.shutdown();  
  
        // 순수 스레드로 구현하는 경우  
        Thread workerThread = new Thread(() -> {  
            log("Runnable 작업 실행");  
            try {  
                Thread.sleep(1000);  
            } catch (InterruptedException e) {  
                Thread.currentThread().interrupt();  
            }  
            int r = 42;  
            log("작업 결과: " + r);  
        });  
  
        // 스레드 시작  
        workerThread.start();  
  
        // 작업을 완료할 때까지 대기  
        workerThread.join();  
  
        log("===============================================");  
        log("모든 작업 완료");  
    }  
}

 

이제 ExecutorService를 통해 작업만 명세해서 전달하면 ExecutorService에서 관리하는 스레드가 작업을 실행하는것을 확인했습니다

작업 중단 및 종료

ExecutorService는 스레드풀을 종료하기 위해 두 가지 메서드를 제공합니다

  • shutdown
  • shutdownNow

두 메서드는 약간의 차이가 있습니다

shutdown: 이전에 제출된 작업은 실행하고 실행중인 스레드를 강제로 인터럽트 하지 않습니다

shutdownNow: 실행중인 스레드를 강제로 인터럽트 하지만 해당 인터럽트의 응답하는 작업이 없는 경우 작업 종료를 보장하지 않습니다

 

두 메서드 모두 shutdown 후 제출시 RejectedExecutionException 예외가 발생합니다

shutdown 호출한 스레드는 실행중인 작업이 종료될때까지 기다리지 않고 바로 다음 라인으로 넘어갑니다. 

작업 종료 대기 및 확인

작업을 종료를 대기하고 확인하는 것을 해보겠습니다

ExecutorService는 작업 종료 대기 및 확인 위해 다음 세 가지 메서드를 제공합니다

  • awaitTermination
  • isShutdown
  • isTerminated

이제 여러가지 예시를 보면서 확인해보겠습니다

먼저 shutdown시 다양한 예시를 살펴보겠습니다

 

shutdown 

 

shutdown후 새로운 작업을 제출하면 RejectedExecutionException가 발생합니다

 

import java.time.Instant;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * timeout 시간 동안 모든 작업이 완료
 * Task :ShutdownWithAwaitTermination.main()
 * 2024-09-02T02:44:38.570685Z|main|ExecutorService 종료 시작
 * 2024-09-02T02:44:38.581398Z|main|2초 동안 awaitTermination 호출
 * 2024-09-02T02:44:40.586993Z|main|모든 태스크가 완료되지 않았습니다. shutdownNow 호출
 * 2024-09-02T02:44:40.587929Z|pool-1-thread-2|작업이 인터럽트되었습니다.
 * 2024-09-02T02:44:40.588786Z|pool-1-thread-1|작업이 인터럽트되었습니다.
 * 2024-09-02T02:44:40.594915Z|main|실행되지 않은 작업 수: 3
 * 2024-09-02T02:44:40.595268Z|main|추가로 2초 동안 awaitTermination 호출
 * 2024-09-02T02:44:40.595370Z|main|ExecutorService 종료 완료
 * 2024-09-02T02:44:40.595451Z|main|메인 스레드 종료
 */

public class ShutdownWithAwaitTermination {
    private static String getCurrentTime() {
        return Instant.now().toString();
    }

    private static void log(String message) {
        System.out.println(getCurrentTime() + "|" + Thread.currentThread().getName() + "|" + message);
    }

    public static void shutdownAndAwaitTermination(ExecutorService pool, long timeout, TimeUnit unit) {
        log("ExecutorService 종료 시작");
        pool.shutdown(); // 새로운 태스크 실행 중지
        try {
            log(unit.toSeconds(timeout) + "초 동안 awaitTermination 호출");
            if (!pool.awaitTermination(timeout, unit)) {
                log("모든 태스크가 완료되지 않았습니다. shutdownNow 호출");
                List<Runnable> notExecutedTasks = pool.shutdownNow(); // 대기 중인 작업 취소
                log("실행되지 않은 작업 수: " + notExecutedTasks.size());

                log("추가로 " + unit.toSeconds(timeout) + "초 동안 awaitTermination 호출");
                if (!pool.awaitTermination(timeout, unit)) {
                    log("ExecutorService가 완전히 종료되지 않았습니다.");
                }
            } else {
                log("모든 태스크가 정상적으로 완료되었습니다.");
            }
        } catch (InterruptedException ie) {
            log("종료 과정 중 인터럽트 발생");
            pool.shutdownNow();
            Thread.currentThread().interrupt();
        }
        log("ExecutorService 종료 완료");
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        for (int i = 0; i < 5; i++) {
            executorService.submit(() -> {
                try {
                    Thread.sleep(3000); // 작업 시간을 3초로 설정
                    log("작업 종료");
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    log("작업이 인터럽트되었습니다.");
                }
                return 42;
            });
        }

        shutdownAndAwaitTermination(executorService, 2, TimeUnit.SECONDS);

        log("메인 스레드 종료");
    }
}

 

실행중인 스레드를 강제로 인터럽트 하지 않기 때문에 인터럽트에 응답하는 작업이나 InterruptedException 예외 구문 작성할 필요없습니다

 

import java.time.Instant;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * 출력
 *
 *  긴 작업이 완료될 때까지 대기
 * > Task :ExecutorServiceShutdownContinueExample.main()
 * 2024-09-02T01:32:18.759786Z|pool-1-thread-1|긴 작업 시작
 * 2024-09-02T01:32:19.764016Z|pool-1-thread-2|짧은 작업 종료
 * 2024-09-02T01:32:19.779332Z|pool-1-thread-1|긴 작업 진행 중... 1초
 * 2024-09-02T01:32:20.765755Z|pool-1-thread-2|짧은 작업 종료
 * 2024-09-02T01:32:20.784259Z|pool-1-thread-1|긴 작업 진행 중... 2초
 * 2024-09-02T01:32:21.765438Z|main|executorService.shutdown() 호출
 * 2024-09-02T01:32:21.766256Z|main|shutdown() 후 새 작업 제출 시도
 * 2024-09-02T01:32:21.769393Z|main|예상대로 RejectedExecutionException 발생: Task java.util.concurrent.FutureTask@4aa298b7[Not completed, task = java.util.concurrent.Executors$RunnableAdapter@6bc7c054[Wrapped task = io.concurrency.chapter10.exam07.ExecutorServiceShutdownContinueExample$$Lambda$6/0x0000000801006000@232204a1]] rejected from java.util.concurrent.ThreadPoolExecutor@7d4991ad[Shutting down, pool size = 2, active threads = 2, queued tasks = 0, completed tasks = 2]
 * 2024-09-02T01:32:21.771411Z|pool-1-thread-2|짧은 작업 종료
 * 2024-09-02T01:32:21.788534Z|pool-1-thread-1|긴 작업 진행 중... 3초
 * 2024-09-02T01:32:22.793373Z|pool-1-thread-1|긴 작업 진행 중... 4초
 * 2024-09-02T01:32:23.798582Z|pool-1-thread-1|긴 작업 진행 중... 5초
 * 2024-09-02T01:32:24.802228Z|pool-1-thread-1|긴 작업 진행 중... 6초
 * 2024-09-02T01:32:25.806359Z|pool-1-thread-1|긴 작업 진행 중... 7초
 * 2024-09-02T01:32:26.808006Z|pool-1-thread-1|긴 작업 진행 중... 8초
 * 2024-09-02T01:32:27.813656Z|pool-1-thread-1|긴 작업 진행 중... 9초
 * 2024-09-02T01:32:28.816178Z|pool-1-thread-1|긴 작업 진행 중... 10초
 * 2024-09-02T01:32:28.816641Z|pool-1-thread-1|긴 작업 완료
 * 2024-09-02T01:32:28.818180Z|main|긴 작업 완료 여부: true
 * 2024-09-02T01:32:28.818393Z|main|스레드 풀 종료 여부: true
 * 2024-09-02T01:32:28.818580Z|main|스레드 풀 완전 종료 여부: true
 * 2024-09-02T01:32:28.818641Z|main|프로그램 종료
 */

public class ExecutorServiceShutdownContinueExample {
    private static String getCurrentTime() {
        return Instant.now().toString();
    }

    private static void log(String message) {
        System.out.println(getCurrentTime() + "|" + Thread.currentThread().getName() + "|" + message);
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        AtomicBoolean longTaskCompleted = new AtomicBoolean(false);

        // 긴 작업 제출
        executorService.submit(() -> {
            log("긴 작업 시작");
            try {
                // 10초 동안 실행되는 작업
                for (int i = 0; i < 10; i++) {
                    Thread.sleep(1000);
                    log("긴 작업 진행 중... " + (i + 1) + "초");
                }
                longTaskCompleted.set(true);
                log("긴 작업 완료");
            } catch (InterruptedException e) {
                log("긴 작업이 인터럽트되었습니다.");
            }
        });

        // 다른 짧은 작업들 제출
        for (int i = 0; i < 3; i++) {
            executorService.submit(() -> {
                try {
                    Thread.sleep(1000);
                    log("짧은 작업 종료");
                } catch (InterruptedException e) {
                    log("짧은 작업이 인터럽트되었습니다.");
                    Thread.currentThread().interrupt();
                }
            });
        }

        // 3초 후 shutdown 호출
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            log("메인 스레드가 인터럽트되었습니다.");
        }

        log("executorService.shutdown() 호출");
        executorService.shutdown();

        // shutdown 후 새 작업 제출 시도
        try {
            log("shutdown() 후 새 작업 제출 시도");
            executorService.submit(() -> {
                log("이 작업은 실행되지 않아야 함");
            });
        } catch (RejectedExecutionException e) {
            log("예상대로 RejectedExecutionException 발생: " + e.getMessage());
        }

        try {
            // 긴 작업이 완료될 때까지 대기 (최대 15초)
            if (!executorService.awaitTermination(15, TimeUnit.SECONDS)) {
                log("타임아웃 발생, executorService.shutdownNow() 호출");
                executorService.shutdownNow();
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        log("긴 작업 완료 여부: " + longTaskCompleted.get());
        log("스레드 풀 종료 여부: " + executorService.isShutdown());
        log("스레드 풀 완전 종료 여부: " + executorService.isTerminated());

        log("프로그램 종료");
    }
}

 

 

shutdown 호출한 스레드는 실행중인 작업이 종료될 때까지 기다리지 않고 바로 다음 라인을 실행한다. 만약 스레드가 메서드 호출 후 블로킹 되기 위해서는 awaitTermination을 사용해야한다.

 

import java.time.Instant;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * > Task :ShutdownContinueExample.main()
 * 2024-09-02T02:25:53.278889Z|pool-1-thread-2|작업 1 실행 중... 1초
 * 2024-09-02T02:25:53.278894Z|pool-1-thread-1|작업 0 실행 중... 1초
 * 2024-09-02T02:25:54.262244Z|main|executorService.shutdown() 호출
 * 2024-09-02T02:25:54.295769Z|pool-1-thread-1|작업 0 실행 중... 2초
 * 2024-09-02T02:25:54.295820Z|pool-1-thread-2|작업 1 실행 중... 2초
 * 2024-09-02T02:25:55.301131Z|pool-1-thread-1|작업 0 실행 중... 3초
 * 2024-09-02T02:25:55.301131Z|pool-1-thread-2|작업 1 실행 중... 3초
 * 2024-09-02T02:25:56.305856Z|pool-1-thread-1|작업 0 실행 중... 4초
 * 2024-09-02T02:25:56.306427Z|pool-1-thread-2|작업 1 실행 중... 4초
 * 2024-09-02T02:25:57.308754Z|pool-1-thread-2|작업 1 실행 중... 5초
 * 2024-09-02T02:25:57.308754Z|pool-1-thread-1|작업 0 실행 중... 5초
 * 2024-09-02T02:25:57.312692Z|pool-1-thread-2|작업 1 정상 종료
 * 2024-09-02T02:25:57.312692Z|pool-1-thread-1|작업 0 정상 종료
 * 2024-09-02T02:25:58.314870Z|pool-1-thread-1|작업 2 실행 중... 1초
 * 2024-09-02T02:25:58.316142Z|pool-1-thread-2|작업 3 실행 중... 1초
 * 2024-09-02T02:25:59.316519Z|pool-1-thread-1|작업 2 실행 중... 2초
 * 2024-09-02T02:25:59.320947Z|pool-1-thread-2|작업 3 실행 중... 2초
 * 2024-09-02T02:26:00.321546Z|pool-1-thread-1|작업 2 실행 중... 3초
 * 2024-09-02T02:26:00.324628Z|pool-1-thread-2|작업 3 실행 중... 3초
 * 2024-09-02T02:26:01.326047Z|pool-1-thread-2|작업 3 실행 중... 4초
 * 2024-09-02T02:26:01.326430Z|pool-1-thread-1|작업 2 실행 중... 4초
 * 2024-09-02T02:26:02.331586Z|pool-1-thread-1|작업 2 실행 중... 5초
 * 2024-09-02T02:26:02.331548Z|pool-1-thread-2|작업 3 실행 중... 5초
 * 2024-09-02T02:26:02.331933Z|pool-1-thread-1|작업 2 정상 종료
 * 2024-09-02T02:26:02.331977Z|pool-1-thread-2|작업 3 정상 종료
 * 2024-09-02T02:26:03.337294Z|pool-1-thread-2|작업 4 실행 중... 1초
 * 2024-09-02T02:26:04.341267Z|pool-1-thread-2|작업 4 실행 중... 2초
 * 2024-09-02T02:26:05.345271Z|pool-1-thread-2|작업 4 실행 중... 3초
 * 2024-09-02T02:26:06.347118Z|pool-1-thread-2|작업 4 실행 중... 4초
 * 2024-09-02T02:26:07.351733Z|pool-1-thread-2|작업 4 실행 중... 5초
 * 2024-09-02T02:26:07.352170Z|pool-1-thread-2|작업 4 정상 종료
 * 2024-09-02T02:26:07.353750Z|main|완료된 작업 수: 5
 * 2024-09-02T02:26:07.354071Z|main|인터럽트된 작업 수: 0
 * 2024-09-02T02:26:07.354772Z|main|스레드 풀 종료 여부: true
 * 2024-09-02T02:26:07.354974Z|main|스레드 풀 완전 종료 여부: true
 * 2024-09-02T02:26:07.355043Z|main|모든 작업이 종료되고 스레드 풀이 종료됨
 */

public class ShutdownContinueExample {
    private static final AtomicInteger completedTasks = new AtomicInteger(0);
    private static final AtomicInteger interruptedTasks = new AtomicInteger(0);

    private static String getCurrentTime() {
        return Instant.now().toString();
    }

    private static void log(String message) {
        System.out.println(getCurrentTime() + "|" + Thread.currentThread().getName() + "|" + message);
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        for (int i = 0; i < 5; i++) {
            final int taskId = i;
            executorService.submit(() -> {
                try {
                    for (int j = 0; j < 5; j++) {  // 각 작업은 5초 동안 실행됨
                        if (Thread.currentThread().isInterrupted()) {
                            log("작업 " + taskId + " 인터럽트 감지됨");
                            interruptedTasks.incrementAndGet();
                            return null;
                        }
                        Thread.sleep(1000);
                        log("작업 " + taskId + " 실행 중... " + (j + 1) + "초");
                    }
                    log("작업 " + taskId + " 정상 종료");
                    completedTasks.incrementAndGet();
                } catch (InterruptedException e) {
                    log("작업 " + taskId + " InterruptedException 발생");
                    interruptedTasks.incrementAndGet();
                    Thread.currentThread().interrupt();
                    return null;
                }
                return 42;
            });
        }

        // 2초 후에 shutdown 호출
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        log("executorService.shutdown() 호출");
        executorService.shutdown();

        try {
            if (!executorService.awaitTermination(20, TimeUnit.SECONDS)) {
                log("executorService.shutdownNow() 호출");
                executorService.shutdownNow();
                log("스레드 풀 강제 종료 수행");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        }

        log("완료된 작업 수: " + completedTasks.get());
        log("인터럽트된 작업 수: " + interruptedTasks.get());
        log("스레드 풀 종료 여부: " + executorService.isShutdown());
        log("스레드 풀 완전 종료 여부: " + executorService.isTerminated());
        log("모든 작업이 종료되고 스레드 풀이 종료됨");
    }
}

 

shutdownNow

현재 실행중인 작업도 중단하려고 시도함, 작업 대기중인 작업 목록 반환 실행중인 스레드를 강제로 인터럽트하고, 해당 작업이 인터럽트에 응답하는 작업이 아닌 경우 작업 종료 보장 안합니다
작업을 종료하기 위해서는 Thread.isInterrupted()나 sleep()과 같은 인터럽트 관련 API를 사용해야 합니다

 

import java.time.Instant;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 실행 중이던 두 작업이 인터럽트를 받습니다.
 * 출력
 * > Task :ExecutorServiceShutdownNowVerification.main()
 * 2024-09-02T02:30:24.587655Z|pool-1-thread-2|인터럽트 무시 작업 시작
 * 2024-09-02T02:30:24.587656Z|pool-1-thread-1|인터럽트 응답 작업 시작
 * 2024-09-02T02:30:25.609929Z|pool-1-thread-1|인터럽트 응답 작업 실행 중... 1초
 * 2024-09-02T02:30:26.611705Z|pool-1-thread-1|인터럽트 응답 작업 실행 중... 2초
 * 2024-09-02T02:30:27.590109Z|main|executorService.shutdownNow() 호출
 * 2024-09-02T02:30:27.590654Z|pool-1-thread-1|인터럽트 응답 작업 인터럽트로 종료
 * 2024-09-02T02:30:27.590655Z|pool-1-thread-2|인터럽트 무시 작업 sleep 중 인터럽트 발생, 무시하고 계속 실행
 * 2024-09-02T02:30:27.592506Z|main|대기 중이던 작업 수: 2
 * 2024-09-02T02:30:34.696887Z|pool-1-thread-2|인터럽트 무시 작업 완료
 * 2024-09-02T02:30:34.698635Z|main|완료된 작업 수: 1
 * 2024-09-02T02:30:34.699102Z|main|인터럽트된 작업 수: 1
 * 2024-09-02T02:30:34.700090Z|main|스레드 풀 종료 여부: true
 * 2024-09-02T02:30:34.700508Z|main|스레드 풀 완전 종료 여부: true
 */

public class ExecutorServiceShutdownNowVerification {
    private static final AtomicInteger completedTasks = new AtomicInteger(0);
    private static final AtomicInteger interruptedTasks = new AtomicInteger(0);

    private static String getCurrentTime() {
        return Instant.now().toString();
    }

    private static void log(String message) {
        System.out.println(getCurrentTime() + "|" + Thread.currentThread().getName() + "|" + message);
    }

    private static Runnable createInterruptibleTask(String name, int duration) {
        return () -> {
            try {
                log(name + " 시작");
                for (int i = 0; i < duration; i++) {
                    Thread.sleep(1000);
                    log(name + " 실행 중... " + (i + 1) + "초");
                }
                log(name + " 완료");
                completedTasks.incrementAndGet();
            } catch (InterruptedException e) {
                log(name + " 인터럽트로 종료");
                interruptedTasks.incrementAndGet();
            }
        };
    }

    private static Runnable createNonInterruptibleTask(String name, int duration) {
        return () -> {
            log(name + " 시작");
            long endTime = System.currentTimeMillis() + duration * 1000;
            while (System.currentTimeMillis() < endTime) {
                if (Thread.currentThread().isInterrupted()) {
                    log(name + " 인터럽트 감지됨, 하지만 계속 실행");
                }
                // CPU를 독점하지 않도록 잠시 대기
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    log(name + " sleep 중 인터럽트 발생, 무시하고 계속 실행");
                }
            }
            log(name + " 완료");
            completedTasks.incrementAndGet();
        };
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        // 실행 중인 작업들
        executorService.submit(createInterruptibleTask("인터럽트 응답 작업", 10));
        executorService.submit(createNonInterruptibleTask("인터럽트 무시 작업", 10));

        // 대기 중인 작업들
        executorService.submit(createInterruptibleTask("대기 작업 1", 5));
        executorService.submit(createInterruptibleTask("대기 작업 2", 5));

        // 3초 후 shutdownNow 호출
        Thread.sleep(3000);

        log("executorService.shutdownNow() 호출");
        List<Runnable> pendingTasks = executorService.shutdownNow();

        log("대기 중이던 작업 수: " + pendingTasks.size());

        // 모든 작업이 종료될 때까지 대기 (최대 15초)
        executorService.awaitTermination(15, TimeUnit.SECONDS);

        log("완료된 작업 수: " + completedTasks.get());
        log("인터럽트된 작업 수: " + interruptedTasks.get());
        log("스레드 풀 종료 여부: " + executorService.isShutdown());
        log("스레드 풀 완전 종료 여부: " + executorService.isTerminated());
    }
}

 

awaitTermination

종료 요청 후 모든 작업이 실행 완료될 때까지 또는 타임아웃이 발생하거나 현재 스레드가 인터럽트될 때까지 블록됩니다
종료가 완료되면 true를 반환하고 종료가 타임아웃 발생 전에 완료되지 않으면 false를 반환합니다

 

awaitTermination은 timeout까지 대기 하다가 종료가 완료되지 않으면 false를 반환합니다. 그래서 반복해서 확인해주어야 하며

해당 로직도 추가시간 이후에도 계속 실행중인 경우에는 스레드의 종료를 대기하지 않습니다. 여기서는 awaitTermination에 대해서만 다루기 때문에 해당 내용은 더 깊게 살펴보지 않겠습니다.

 

/**
 * 출력
 * > Task :ShutdownWithAwaitTerminationTimeoutExample.main()
 * 2024-09-02T02:46:43.551272Z|main|ExecutorService 종료 시작
 * 2024-09-02T02:46:43.555996Z|pool-1-thread-2|Task 2: 긴 작업 시작 (5초)
 * 2024-09-02T02:46:43.555974Z|pool-1-thread-1|Task 1: 긴 작업 시작 (5초)
 * 2024-09-02T02:46:43.563090Z|main|3초 동안 awaitTermination 호출
 * 2024-09-02T02:46:46.567602Z|main|지정된 시간 내에 모든 태스크가 완료되지 않았습니다. shutdownNow 호출
 * 2024-09-02T02:46:46.569091Z|main|실행되지 않은 작업 수: 3
 * 2024-09-02T02:46:46.569401Z|pool-1-thread-1|Task 1: 작업이 인터럽트되었습니다.
 * 2024-09-02T02:46:46.569506Z|pool-1-thread-2|Task 2: 작업이 인터럽트되었습니다.
 * 2024-09-02T02:46:46.569585Z|main|추가로 3초 동안 awaitTermination 호출
 * 2024-09-02T02:46:46.570342Z|main|ExecutorService 종료 완료
 * 2024-09-02T02:46:46.570444Z|main|메인 스레드 종료
 */

public class ShutdownWithAwaitTerminationTimeoutExample {
    private static String getCurrentTime() {
        return Instant.now().toString();
    }

    private static void log(String message) {
        System.out.println(getCurrentTime() + "|" + Thread.currentThread().getName() + "|" + message);
    }

    public static void shutdownAndAwaitTermination(ExecutorService pool, long timeout, TimeUnit unit) {
        log("ExecutorService 종료 시작");
        pool.shutdown(); // 새로운 태스크 실행 중지
        try {
            log(unit.toSeconds(timeout) + "초 동안 awaitTermination 호출");
            if (!pool.awaitTermination(timeout, unit)) {
                log("지정된 시간 내에 모든 태스크가 완료되지 않았습니다. shutdownNow 호출");
                List<Runnable> notExecutedTasks = pool.shutdownNow(); // 대기 중인 작업 취소
                log("실행되지 않은 작업 수: " + notExecutedTasks.size());

                log("추가로 " + unit.toSeconds(timeout) + "초 동안 awaitTermination 호출");
                if (!pool.awaitTermination(timeout, unit)) {
                    log("ExecutorService가 완전히 종료되지 않았습니다.");
                }
            } else {
                log("모든 태스크가 정상적으로 완료되었습니다.");
            }
        } catch (InterruptedException ie) {
            log("종료 과정 중 인터럽트 발생");
            pool.shutdownNow();
            Thread.currentThread().interrupt();
        }
        log("ExecutorService 종료 완료");
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        // 5개의 작업 제출: 2개는 긴 작업, 3개는 짧은 작업
        for (int i = 0; i < 5; i++) {
            final int taskNumber = i + 1;
            executorService.submit(() -> {
                try {
                    if (taskNumber <= 2) {
                        log("Task " + taskNumber + ": 긴 작업 시작 (5초)");
                        Thread.sleep(5000); // 긴 작업 (5초)
                    } else {
                        log("Task " + taskNumber + ": 짧은 작업 시작 (1초)");
                        Thread.sleep(1000); // 짧은 작업 (1초)
                    }
                    log("Task " + taskNumber + ": 작업 완료");
                } catch (InterruptedException e) {
                    log("Task " + taskNumber + ": 작업이 인터럽트되었습니다.");
                    Thread.currentThread().interrupt();
                }
                return taskNumber;
            });
        }

        // 3초 타임아웃으로 종료 프로세스 시작
        shutdownAndAwaitTermination(executorService, 3, TimeUnit.SECONDS);

        log("메인 스레드 종료");
    }
}