Java

[Java] 멀티쓰레드 Callable, Executor, ExecutorService 알아보기

하부루 2024. 11. 30. 14:40

Runnable 인터페이스

Runnable 인터페이스의 run()메서드는 void 타입을 반환하므로 결과를 반환할 수 없다는 단점이 있습니다. 반환 값을 얻기위해 복잡한 작업을 거쳐야하는데 그 작업이 매우 번거롭기에 이러한 문제를 개선하기 위해 추가된 Callable과 Future가 있습니다.

@FunctionalInterface
public interface Runnable {
    void run();
}
public class RunnableExample {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // Runnable을 실행할 스레드 생성
        Runnable task = new Runnable() {
            @Override
            public void run() {
                try {
                    // 작업 처리
                    Thread.sleep(1000);
                    System.out.println("작업 완료!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        // ExecutorService 생성
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        
        // Runnable 작업 제출 (결과를 반환할 수 없음)
        executorService.submit(task);

        // Executor 종료
        executorService.shutdown();

        // 결과를 반환하려면 작업 종료 후 다른 방법을 사용해야 함
        // 예를 들어, 결과를 담을 객체를 사용하거나, 별도의 상태 변수로 결과를 관리해야 함
    }
}

 

Callable 인터페이스

Runnable의 문제점을 해결하기위해 Java5에 제네릭타입을 사용해 결과를 받을 수 있는 Callable 인터페이스가 추가되었습니다. 정리하면 Runnable과 Callable의 차이점은 작업한 내용의 결과를 반환 받을 수 있냐? 없냐? 입니다.

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}
public class CallableExample {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // Callable 작업 생성
        Callable<String> task = new Callable<String>() {
            @Override
            public String call() throws Exception {
                Thread.sleep(1000);  // 작업 대기
                return "작업 완료!";  // 결과 반환
            }
        };

        // ExecutorService 생성
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        
        // Callable 작업 제출하고 결과를 Future로 받음
        Future<String> future = executorService.submit(task);

        // 결과를 얻기 위해 get() 메서드 호출 (결과가 준비될 때까지 대기)
        System.out.println(future.get());

        // Executor 종료
        executorService.shutdown();
    }
}

 

Future 인터페이스

Future 인터페이스는 비동기 작업의 결과를 나타내는 객체입니다. 완료된 Callable의 반환 값을 구하기 위해 사용되는 것이 Future입니다. Future는 다음과 같은 메서드를 가지고 있습니다.

public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException, ExecutionException;

    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}
  • get()
    • 작업이 완료될 때까지 기다리고 결과를 반환. 만약 작업이 아직 완료되지 않았다면, get()은 해당 작업이 끝날 때까지 블록된다.
  • get(long timeout, TimeUnit unit)
    • 지정한 시간 내에 작업이 완료되지 않으면 예외를 던진다.
  • cancel()
    • 현재 작업을 취소.
  • isDone()
    • 작업이 완료되었으면 true를 반환.
  • isCancelled()
    • 작업이 취소되었으면 true를 반환.

 

Executor 인터페이스

동시에 여러 요청을 해결해야 하는 경우에 매번 새로운 쓰레드를 생성하는 것은 비효율적입니다. 그래서 쓰레드를 미리 만들어두고 재사용하는 쓰레드 풀(Thread Pool)이 사용되는데 Executor 인터페이스는 비동기 작업을 실행시키는 역할만을 담당합니다.

public interface Executor {
    void execute(Runnable command);
}

 

ExecutorService 인터페이스

ExecutorService 인터페이스는 멀티스레딩을 다루기 위한 중요한 인터페이스로, Executor의 확장된 형태입니다. ExecutorService는 작업 실행을 넘어서, 작업 결과 처리, 작업 취소, 작업 종료정리 작업 등을 위한 추가적인 기능을 제공합니다. 이를 통해 비동기 작업을 더 효율적이고 관리 가능한 방식으로 처리할 수 있습니다. ExecutorService는 다음과 같은 메서드를 가지고 있습니다.

public interface ExecutorService extends Executor {

    void shutdown();

    List<Runnable> shutdownNow();

    boolean isShutdown();

    boolean isTerminated();

    boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException;

    <T> Future<T> submit(Callable<T> task);

    <T> Future<T> submit(Runnable task, T result);

    Future<?> submit(Runnable task);

    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;

    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                  long timeout, TimeUnit unit)
        throws InterruptedException;

    <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException;

    <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                    long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}
  • submit()
    • submit() 메서드는 Runnable 또는 Callable 작업을 제출하고, 결과를 Future 객체로 반환한다. Future 객체를 통해 결과를 나중에 확인하거나 취소가능.
  • invokeAll()
    • 여러 개의 Callable 작업을 동시에 제출하고, 모든 작업이 완료되기를 기다립니다. 반환값은 Future 객체의 목록 값 이다.
  • invokeAny()
    • 여러 개의 Callable 작업 중 하나라도 성공적으로 완료되면, 그 작업의 결과를 반환한다. 실패한 작업들은 자동으로 취소.
  • shutdown()
    • shutdown()은 ExecutorService를 종료하라는 명령을 내린다. 이미 제출된 작업은 실행되지만, 새로운 작업은 받을 수 없다. 모든 작업이 완료되면 자원을 정리.
  • shutdownNow()
    • shutdownNow()는 실행 중인 작업을 즉시 중단하고, 대기 중인 작업을 취소하려 시도. 이 메서드는 비정상 종료를 유발할 수 있기 때문에 사용에 주의가 필요하다.

 

ExecutorService 과 Callable를 사용한 예제코드

public class ExecutorServiceCallableExample {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // ExecutorService 생성 (스레드 풀 크기 2로 설정)
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        // Callable 작업 정의
        Callable<Integer> task1 = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("작업 1 시작 - " + Thread.currentThread().getName());
                Thread.sleep(1000); // 1초 대기
                return 10; // 작업 결과 반환
            }
        };

        Callable<Integer> task2 = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("작업 2 시작 - " + Thread.currentThread().getName());
                Thread.sleep(500); // 0.5초 대기
                return 20; // 작업 결과 반환
            }
        };

        // 작업 제출 및 Future 객체 받아오기
        Future<Integer> future1 = executorService.submit(task1);
        Future<Integer> future2 = executorService.submit(task2);

        // Future 객체에서 결과를 가져옴
        Integer result1 = future1.get(); // 작업 1의 결과
        Integer result2 = future2.get(); // 작업 2의 결과

        System.out.println("작업 1 결과: " + result1);
        System.out.println("작업 2 결과: " + result2);

        // ExecutorService 종료
        executorService.shutdown();
    }
}

== 결과 값 ==
작업 1 시작 - pool-1-thread-1
작업 2 시작 - pool-1-thread-2
작업 2 완료 - pool-1-thread-2
작업 1 완료 - pool-1-thread-1
작업 1 결과: 10
작업 2 결과: 20