개발/자바

Thread ExecutorService API

피터JK 2025. 2. 17. 14:48
728x90

ExecutorService 설명

ExecutorService는 Java의 스레드 풀(Thread Pool) 을 제공하는 인터페이스로, 직접 Thread 객체를 생성하는 대신 효율적으로 스레드를 관리할 수 있게 해줍니다.


1. ExecutorService의 개념

  • 스레드 풀(Thread Pool) 을 관리하는 고급 API
  • 스레드를 미리 생성하여 재사용하는 방식
  • execute()와 submit()을 통해 작업을 실행 가능
  • 스레드 생성 비용 절감 및 성능 향상
  • Deadlock 방지, 스레드 개수 제한 등의 효과

🔹 사용하지 않으면?

for (int i = 0; i < 5; i++) {
    Thread thread = new Thread(new Task());
    thread.start();
}

이 방식은 매번 새로운 Thread를 생성하여 비효율적이고, 많은 리소스를 소비함.

🔹 ExecutorService 사용하면?

ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
    executor.submit(new Task());
}
executor.shutdown();

👉 스레드를 재사용하여 성능 최적화 가능!


2. 주요 메서드

메서드 설명
execute(Runnable command) 작업 실행 (반환값 없음)
submit(Callable/Vunnable task) 작업 실행 후 Future 반환
shutdown() 기존 작업 수행 후 종료
shutdownNow() 즉시 종료 (대기 중인 작업 취소)
isShutdown() shutdown() 호출 여부 확인
isTerminated() 모든 작업 종료 여부 확인
invokeAll(Collection tasks) 모든 작업을 실행하고, 결과 리스트 반환
invokeAny(Collection tasks) 가장 먼저 완료된 작업의 결과 반환

3. 다양한 스레드 풀 생성 방법

Executors 클래스를 사용하여 다양한 방식의 스레드 풀을 생성할 수 있음.

(1) 고정 크기 스레드 풀 (FixedThreadPool)

ExecutorService executor = Executors.newFixedThreadPool(3);
  • 3개의 스레드만 사용
  • 제한된 개수의 작업을 처리하는 경우 적합
  • 과부하 방지에 유용

(2) 캐시 스레드 풀 (CachedThreadPool)

ExecutorService executor = Executors.newCachedThreadPool();
  • 필요한 만큼 동적으로 스레드 생성
  • 유휴(Idle) 상태가 되면 스레드 자동 삭제
  • 작업량이 변동하는 경우 적합
  • 단점: 너무 많은 스레드가 생성될 가능성이 있음

(3) 단일 스레드 풀 (SingleThreadExecutor)

ExecutorService executor = Executors.newSingleThreadExecutor();
  • 하나의 스레드로 작업을 순차 실행
  • 작업 순서를 유지해야 하는 경우 적합
  • 예: 로그 처리, 파일 I/O 등

(4) 일정 시간 간격으로 실행되는 스레드 풀 (ScheduledThreadPool)

ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
executor.schedule(() -> System.out.println("3초 후 실행!"), 3, TimeUnit.SECONDS);
  • 특정 시간 후 실행 or 주기적 실행 가능
  • 스케줄링 작업에 적합

4. 실행 예제

(1) FixedThreadPool 예제

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class Task implements Runnable {
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 작업 실행");
    }
}

public class FixedThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 5; i++) {
            executor.submit(new Task());
        }

        executor.shutdown();
    }
}

출력 예시:

pool-1-thread-1 작업 실행
pool-1-thread-2 작업 실행
pool-1-thread-3 작업 실행
pool-1-thread-1 작업 실행
pool-1-thread-2 작업 실행

설명:

  • 3개의 스레드가 5개의 작업을 나눠서 처리
  • 스레드 재사용

(2) Callable & Future 사용 (값 반환)

Runnable은 반환값이 없지만, Callable은 값을 반환할 수 있음.

import java.util.concurrent.*;

class MyCallable implements Callable<String> {
    public String call() {
        return "작업 완료!";
    }
}

public class CallableExample {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<String> future = executor.submit(new MyCallable());

        System.out.println(future.get()); // 블로킹 호출
        executor.shutdown();
    }
}

출력:

작업 완료!
  • Future.get()을 호출하면 결과가 나올 때까지 블로킹됨

5. shutdown() vs shutdownNow()

🔹 shutdown()

  • 기존 작업을 모두 수행한 후 종료
  • 추가 작업 요청은 거부됨
executor.shutdown();

🔹 shutdownNow()

  • 즉시 실행 중인 작업을 중단 시도
  • 대기 중인 작업은 취소됨
executor.shutdownNow();

6. invokeAll()과 invokeAny()

(1) invokeAll()

  • 여러 Callable을 실행하고, 모든 작업이 완료될 때까지 대기
  • 결과를 List<Future>로 반환
import java.util.concurrent.*;

public class InvokeAllExample {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        Callable<String> task1 = () -> "Task 1 완료";
        Callable<String> task2 = () -> "Task 2 완료";
        Callable<String> task3 = () -> "Task 3 완료";

        var futures = executor.invokeAll(List.of(task1, task2, task3));

        for (Future<String> future : futures) {
            System.out.println(future.get());
        }

        executor.shutdown();
    }
}

출력:

Task 1 완료
Task 2 완료
Task 3 완료

(2) invokeAny()

  • 여러 Callable 중 가장 먼저 끝난 작업의 결과만 반환
  • 나머지 작업은 취소됨
import java.util.concurrent.*;

public class InvokeAnyExample {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        Callable<String> task1 = () -> {
            Thread.sleep(2000);
            return "Task 1 완료";
        };

        Callable<String> task2 = () -> {
            Thread.sleep(1000);
            return "Task 2 완료";
        };

        Callable<String> task3 = () -> {
            Thread.sleep(3000);
            return "Task 3 완료";
        };

        String result = executor.invokeAny(List.of(task1, task2, task3));

        System.out.println("가장 빨리 끝난 작업: " + result);

        executor.shutdown();
    }
}

출력:

가장 빨리 끝난 작업: Task 2 완료

👉 Task 2가 가장 먼저 끝났기 때문에 나머지는 취소됨.


7. 결론

방식 특징
FixedThreadPool 고정된 개수의 스레드 유지
CachedThreadPool 필요할 때마다 스레드 생성 (최적화)
SingleThreadExecutor 단일 스레드로 순차적 실행
ScheduledThreadPool 일정 간격/지연 시간 후 실행
invokeAll() 모든 작업 완료 후 결과 리스트 반환
invokeAny() 가장 먼저 끝난 작업 결과만 반환

💡 ExecutorService를 활용하면 멀티스레드 관리를 쉽게 할 수 있습니다! 

728x90