자바 스레드 ExecutorService

바그다드·2023년 7월 7일

ExecutorService란?

스레드를 관리하고 실행하기 위한 인터페이스이다.
스레드풀을 생성하고 관리하는 기능을 제공하며, 비동기 작업을 실행하고 완료된 작업의 결과를 리턴 받을 수 있도록 도와준다.

1. 싱글스레드

public class ExcutorServiceRunner {
    public static void main(String[] args) {
        // 싱글스레드 제어자
        ExecutorService executorService = Executors.newSingleThreadExecutor();

        executorService.execute(new Task1());
        
        executorService.execute(new Thread(new Task2()));

        // task3
        System.out.print("task3 kicked off \n");
        for (int i = 301; i <= 399; i++) {
            System.out.print(i +" ");
        }
        System.out.println("\n task3 done");
        System.out.println("main done");

        executorService.shutdown();
    }
}
  • Executors.newSingleThreadExecutor();
    메서드 이름처럼 싱글스레드 제어자를 생성한다.

    결과를 확인해보면 task3와 task1,2은 서로 병렬로 처리되고,
    task1과 task2는 싱글스레드로 순서대로 처리되는 것을 확인할 수 있다.

  • executorService.execute(new Task1());
    executorService.execute(new Thread(new Task2()));
    스레드 객체를 직접 인자로 전달하거나 스레드로 감싸서 전달하는 방법이 있다.
    두 방법 다 스레드풀에서 관리하기 때문에 간단하게 첫번째 방법을 쓰는게 편하다.

  • executorService.shutdown();
    스레드를 종료하는 메서드
    이 shutdown() 메서드를 호출해주지 않으면 백그라운드에서 스레드가 실행되기 때문에 작업이 끝난 후에는 꼭 호출해주자!!!

2. 멀티스레드

class Task extends Thread {

    private int number;

    public Task(int number) {
        this.number = number;
    }

    @Override
    public void run() { // 이부분은 이대로 사용해줘야함
        System.out.print("\nTask " + number + " Start");

        for (int i = number*100; i <= number*100+99; i++) {
            System.out.print(i +" ");
        }

        System.out.print("\nTask" + number + " done");
    }
}

public class ExcutorServiceRunner {
    public static void main(String[] args) {
        // 멀티스레드 제어자
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        executorService.execute(new Task(1));
        executorService.execute(new Task(2));
        executorService.execute(new Task(3));
        executorService.execute(new Task(4));
        executorService.execute(new Task(5));
        executorService.execute(new Task(6));
        executorService.execute(new Task(7));
        executorService.execute(new Task(8));

        executorService.shutdown();
    }
}
  • Executors.newFixedThreadPool(5);
    5개의 스레드를 가진 멀티스레드 제어자를 생성한다.

결과를 확인해보면

각 스레드가 병렬로 처리가 되고, 특정 시점에 실행중인 스레드의 최대 수는 5개인 것을 확인할 수 있다!!

3. 스레드 단일 반환값 받기

  • Callable인터페이스를 상속받으면 반환값 생성할 수 있다.
class CallableTask implements Callable<String> {

    private String name;

    public CallableTask(String name) {
        this.name = name;
    }

    @Override
    public String call() throws Exception {
        Thread.sleep(1000);
        return "Hello" + name;
    }
}

public class CallableRunner {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        Future<String> welcomeFuture = executorService.submit(new CallableTask("qkrmekem"));

        System.out.println("new CallableTask executed");

        String welcomeMessage = welcomeFuture.get();

        System.out.println(welcomeMessage);

        System.out.println("\n Main completed");

        executorService.shutdown();
    }
}
  • Future< String> welcomeFuture = executorService.submit(new CallableTask("qkrmekem"));
    여기서 Futures라는 것은 이후 특정 시점에 결과가 도출될 것을 약속하는, 일종의 계약 개념이다.
    이 시점에 일단 스레드가 실행이 된다.
  • String welcomeMessage = welcomeFuture.get();
    스레드에서 결과값을 반환받는 메서드로 이 시점에 스레드의 결과값을 받기 위해 스레드의 작업이 완료되기를 기다린다.
  • 처리되는 상황을 눈으로 확인하기 위해 sleep(1000)을 설정하였다.
    결과를 확인해보자

    실제로 실행해보면 두번째 출력문이 출력되기 전에 1초정도 텀이 있는 것을 확인할 수 있는데,
    executorService.submit(new CallableTask("qkrmekem"));으로 스레드를 실행하고,
    첫번째 출력문을 출력할 때까지 병렬로 처리가 되다가,
    Future객체에서 get()을 이용해 값을 반환받기 위해 스레드의 작업이 완료될 때까지 기다린다.
    스레드의 작업이 완료되어 반환값을 전달 받으면 그 이후에 두 번째, 세 번째 출력문이 출력되는 것을 확인할 수 있다.

4. 스레드 다중값 반환 받기

public class MultipleCallableRunner {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executorService = Executors.newFixedThreadPool(1);

        List<CallableTask> callableTasks = List.of(new CallableTask("qkrmekem"),
                new CallableTask("test"), new CallableTask("admin"));

        List<Future<String>> futures = executorService.invokeAll(callableTasks);

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

        executorService.shutdown();
    }
}
  • List<Future< String>> futures = executorService.invokeAll(callableTasks);
    invokeAll()메서드에 Collection을 인자로 넘겨 여러 값을 반환받을 수 있다.
    여기서는 3개의 Callable객체를 담은 리스트를 넘겼다.
  • invokeAll을 호출하면 스레드를 호출하고 각 스레드가 완료되어 반환값을 받을 때까지 대기한다.
    여기서는 하나의 스레드를 가진 스레드풀로 3개의 스레드를 처리하고 있어 3초가량의 딜레이 후 각 값이 출력된다.
  • Executors.newFixedThreadPool(3);
    이처럼 스레드를 3개로 늘려주면 각 스레드를 병렬로 처리하기 때문에 1초가량의 딜레이 후에 값이 출력된다.

5. 다중 스레드에서 가장 빨리 반환되는 값만 받아오기

public class MultipleAnyCallableRunner {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        List<CallableTask> callableTasks = List.of(new CallableTask("qkrmekem"),
                new CallableTask("test"), new CallableTask("admin"));

        String futures = executorService.invokeAny(callableTasks);

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

        executorService.shutdown();
    }
}
  • executorService.invokeAny(callableTasks);
    멀티스레드 중에서 작업이 가장 빨리 끝난 값을 반환한다.
    하나의 값만 반환하기 때문에 반환 타입으로 값을 받으면 된다.


이처럼 실행할 때마다 반환값이 달라지는 것을 확인할 수 있다.

이것으로 자바에서 스레드를 생성하고 다루는 방법에 대해서 알아보았다. 스프링을 공부하면서 스프링이 주는 편리함과 각 기술이 어떻게 구현되었는지에 대해서 알아보았지만 그보다 더 바탕이되는 자바를 직접 공부해야 하지 않나 하는 생각을 했는데, 막상 이렇게 자바 기술 자체를 알아보고 멀티스레드를 직접 다뤄보니 꽤 재밌는 시간이었다ㅎㅎ

profile
꾸준히 하자!

0개의 댓글