Language/Java / / 2023. 8. 1. 14:12

[Java] Executors / ExecutorService 사용법

반응형

자바 Thread 관리의 어려움

  • 자바로 스레드를 생성할 경우에는 아주 기본적으로는 아래와 같이 Thread 클래스와 Runnable 함수형 인터페이스를 구현해 Thread를 생성합니다.
  • 간단한 소스 같은 경우에는 쉽게 관리할 수 있지만 복잡해지는 경우에는 스레드를 사용자가 직접 관리하는 것은 매우 어렵습니다.
  • ex) 인터럽트 관리 
  • 이러한 관리의 어려운 문제를 해결하기 위해 스레드를 만들고 관리하는 작업을 위임을 하기 위해 Executors가 등장하게 됩니다.
// 람다로 스레드 만들기
Thread thread = new Thread(() -> {
    System.out.println("Thread Test " + Thread.currentThread().getName());
});
thread.start();

System.out.println("Main Test " + Thread.currentThread().getName()); // main 스레드

 

Executors

  • Thread를 만들고 관리하는 것을 고수준의 API Executors에게 위임합니다.
  • Runnable만 개발자가 만들고 생성, 종료, 없애기 작업(일련의 작업)들은 Executors가 해줍니다.
  • 인터페이스는 크게 Executor와 ExecutorService가 있으나 실질적으로 ExecutorService를 사용합니다.
  • 개발자는 작업만 소스코드로 작성하면 됩니다.
  • java.util.concurrent.Executors와 java.util.concurrent.ExecutorService를 이용하면 간단히 쓰레드풀을 생성하여 병렬처리를 할 수 있습니다.

 

ExecutorService 생성

Executors는 ExecutorService 객체를 생성하며, 다음 메소드를 제공하여 쓰레드 풀을 개수 및 종류를 정할 수 있습니다.

 

  • new FixedThreadPool(int) : 인자 개수만큼 고정된 쓰레드풀을 만듭니다.
  • new CachedThreadPool(): 필요할 때, 필요한 만큼 쓰레드풀을 생성합니다. 이미 생성된 쓰레드를 재활용할 수 있기 때문에 성능상의 이점이 있을 수 있습니다.
  • new ScheduledThreadPool(int): 일정 시간 뒤에 실행되는 작업이나, 주기적으로 수행되는 작업이 있다면 ScheduledThreadPool을 고려해볼 수 있습니다.
  • new SingleThreadExecutor(): 쓰레드 1개인 ExecutorService를 리턴합니다. 싱글 쓰레드에서 동작해야 하는 작업을 처리할 때 사용합니다.

 

다음은 4개의 고정된 쓰레드풀을 갖고 있는 ExecutorService를 생성하는 코드입니다.

ExecutorService executor = Executors.newFixedThreadPool(4);

 

ExecutorService

  • Executor 상속받은 인터페이스로, Callable도 실행할 수 있으며, Executor를 종료시키거나, 여러 Callable을 동시에 실행하는 등의 기능을 제공한다.
  • Runnable은 리턴 값이 없습니다. 그러나 Callable은 특정 타입의 객체를 리턴할 수 있습니다. (리턴 유무의 차이)
    • public interface Runnable { public abstract void run(); }
    • public interface Callable<V> { V call() throws Exception; }
  • Thread 사용 시 실질적으로 사용하는 인터페이스입니다.
  • 주로 Executors 클래스의 Static Method를 활용해 구현하여 사용합니다.
package java.util.concurrent;

import java.util.Collection;
import java.util.List;

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);
    
    // (. . .) 생략
}

 

3. 예시

SingleThread

  • ExecutorService 인터페이스와 Executors 클래스의 static method를 이용해 ExecutorService를 구현하여 사용합니다.
  • 사용 종료 후에는 종료 명령어를 이용해서 종료해야 합니다. 그렇지 않을 경우엔 다음 작업이 들어올 때까지 무한 대기를 합니다.

 

[주요 소스코드]

1. 구현체 생성 -> Executors.newSingleThreadExecutor()

: Executors 클래스의 Static Method를 활용하여 ExecutorService 구현체를 SingleThread 형태로 리턴해줍니다.

 

2. 작업 제출 -> submit()

: Thread를 활용할 작업을 제출합니다.(해당 스레드가 대기 중인 경우 제출한 작업을 처리합니다.)

 

3. 작업 종료 -> shutdown()

: 현재 진행 중인 작업을 마치고 Thread를 종료합니다. (꼭 종료해야 합니다. 그렇지 않을 경우 종료하지 않고 무한 대기합니다.)

 

즉시 종료 -> shutdownNow()

: 현재 진행 중인 작업을 마치지 않은 채로 종료할 수도 있습니다.(즉시 종료)

 

ExecutorService에 작업(Job) 추가

Executors로 ExecutorService를 생성하였다면, ExecutorService는 작업을 처리할 수 있습니다. ExecutorService.submit() 메소드로 작업을 추가하면 됩니다.

 

아래 코드에서 newFixedThreadPool(4)는 Thread를 4개 생성하겠다는 의미입니다. 그리고 submit(() -> { })은 멀티쓰레드로 처리할 작업을 예약합니다. 인자로 람다식을 전달할 수 있습니다.

 

아래 코드에서 4개의 작업을 예약했고, 예약과 동시에 먼저 생성된 4개의 쓰레드는 작업들을 처리합니다.

 

package test;
 
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
 
public class ExecutorServiceTest {
 
    public static void main(String args[]) throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(4);
 
        executor.submit(() -> {
            String threadName = Thread.currentThread().getName();
            System.out.println("Job1 " + threadName);
        });
        executor.submit(() -> {
            String threadName = Thread.currentThread().getName();
            System.out.println("Job2 " + threadName);
        });
        executor.submit(() -> {
            String threadName = Thread.currentThread().getName();
            System.out.println("Job3 " + threadName);
        });
        executor.submit(() -> {
            String threadName = Thread.currentThread().getName();
            System.out.println("Job4 " + threadName);
        });
 
        // 더이상 ExecutorService에 Task를 추가할 수 없습니다.
        // 작업이 모두 완료되면 쓰레드풀을 종료시킵니다.
        executor.shutdown();
 
        // shutdown() 호출 전에 등록된 Task 중에 아직 완료되지 않은 Task가 있을 수 있습니다.
        // Timeout을 20초 설정하고 완료되기를 기다립니다.
        // 20초 전에 완료되면 true를 리턴하며, 20초가 지나도 완료되지 않으면 false를 리턴합니다.
        if (executor.awaitTermination(20, TimeUnit.SECONDS)) {
            System.out.println(LocalTime.now() + " All jobs are terminated");
        } else {
            System.out.println(LocalTime.now() + " some jobs are not terminated");
 
            // 모든 Task를 강제 종료합니다.
            executor.shutdownNow();
        }
 
        System.out.println("end");
    }
}

 

로그를 보면 두개의 쓰레드가 4개의 작업을 모두 처리하였습니다. 대부분 예약한 순서대로 작업이 처리가 되지만, 간혹 쓰레드가 지연이 되어 순서가 뒤바뀌는 일이 발생할 수 있습니다.

 

shutdown()은 더 이상 쓰레드풀에 작업을 추가하지 못하도록 합니다. 그리고 처리 중인 Task가 모두 완료되면 쓰레드풀을 종료시킵니다.

 

awaitTermination()은 이미 수행 중인 Task가 지정된 시간동안 끝나기를 기다립니다. 지정된 시간 내에 끝나지 않으면 false를 리턴하며, 이 때 shutdownNow()를 호출하면 실행 중인 Task를 모두 강제로 종료시킬 수 있습니다.


MultiThread

  • 위와 같이 마찬가지로 Executors 클래스의 static method를 이용해 ExecutorService를 구현하여 사용합니다.

 

[주요 소스코드]

1. 구현체 생성 -> Executors.newFixedThreadPool(Thread 개수)

: Executors 클래스의 Static Method를 활용하여 ExecutorService 구현체를 MultiThread 형태로 리턴해줍니다.

 

2. 작업 제출 -> submit()
: Thread를 활용할 작업을 제출합니다.(해당 스레드가 대기중인 경우 제출한 작업을 처리합니다.)

 

3. 작업 종료 -> shutdown()
: 현재 진행 중인 작업을 마치고 Thread를 종료합니다. (꼭 종료해야 합니다. 그렇지 않을 경우 종료하지 않고 무한 대기합니다.)

 

★ 스레드는 2개인데 작업은 4개라면 어떻게 되나요?

: 그럴 경우 스레드에서 작업을 바로 처리를 못하게 됩니다.

처리하지 못한 작업은 Blocking Queue에 작업을 쌓아서 대기해 둔 상태 두고 앞의 작업이 끝난 후에 작업을 처리합니다.

 

[예시]

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

    executorService.submit(addRunnable(1,2));
    executorService.submit(addRunnable(1,3));
    executorService.submit(addRunnable(1,4));
    executorService.submit(addRunnable(1,5));

    executorService.shutdown();
}

private static Runnable addRunnable(int num1, int num2) {
    return () -> System.out.println("result: " + (num1 + num2) + " (" + Thread.currentThread().getName() + ") ");
}

 

[실행 결과]

  • 실행 결과를 보면 스레드 사용 시 실행 순서는 보장되지 않는 것을 확인할 수 있습니다. (아래의 소스코드가 먼저 실행되는 경우도 있습니다.)

 

반응형

'Language > Java' 카테고리의 다른 글

[Java] 동시성 이슈 해결하기  (0) 2023.08.01
[Java] CountDownLatch  (0) 2023.08.01
Enum(EnumMap, EnumSet)  (0) 2023.07.06
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유