본문으로 바로가기

[Java] Executors Thread 사용법

category Backend/Java 2022. 1. 6. 12:39

1. 자바 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 스레드

 

2. Executors

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

 

Executors의 역할

  1. Thread 만들기: 애플리케이션이 사용할 Thread pool을 만들어 관리합니다.
  2. Thread 관리: Thread 생명 주기를 관리합니다.
  3. 작업 처리 및 실행: Thread로 실행할 작업을 제공할 수 있는 API를 제공합니다.

 

주요 인터페이스

[Executor]

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

 

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

 

[ScheduledExecutorService]

  • ExecutorService를 상속받은 인터페이스로 특정 시간 이후에 또는 주기적으로 작업을 실행할 수 있습니다.
package java.util.concurrent;

public interface ScheduledExecutorService extends ExecutorService {

    public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);

    public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);

    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit);
                                                  
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit);
}

 

3. 예시

SingleThread

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

[주요 소스코드]

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

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

 

2. 작업 제출 -> submit()

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

 

3. 작업 종료 -> shutdown()

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

 

즉시 종료 -> shutdownNow()

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

 

[예시]

public static void SingleThread() {
    // 1. [Single Thread ExecutorService 구현체 생성]
    ExecutorService executorService = Executors.newSingleThreadExecutor();

    // 2. [Runnable 작업 제출]
    executorService.submit(() -> System.out.println("Thread Test: " + Thread.currentThread().getName()));

    // 3. [graceful shutdown]
    executorService.shutdown();

    // [즉시 종료]
    executorService.shutdownNow();
}

[실행 결과]

싱글 스레드 실행결과


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() + ") ");
}

 

[실행 결과]

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

멀티 스레드 실행결과


ScheduledThread

  • 특정 시간 이후에 또는 주기적(반복적)으로 작업을 실행할 수 있습니다. 
  • 크론이나 스케줄러처럼 사용할 수도 있습니다.

[예시 1] -> 10초 대기 후에 작업 처리(실행)

public static void main(String[] args) {
    ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();

    System.out.println("실행: " + LocalDateTime.now());

    // 매개변수 -> (Runnable, 대기시간, 시간단위)
    scheduledExecutorService.schedule(printRunnable("스레드 테스트"), 10, TimeUnit.SECONDS); // 3초 정도 있다가 실행하라
   
    scheduledExecutorService.shutdown();
}

[실행 결과]

예시 1

 

[예시 2] -> 1초 대기 후 10초에 1번씩 작업 처리(실행)

public static void main(String[] args) {
    ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();

    System.out.println("실행: " + LocalDateTime.now());

    // 1초 기다렸다가 10초에 한 번씩 실행 (shutdown 지우기) (Runnable, 대기시간, 주기, 단위)
    scheduledExecutorService.scheduleAtFixedRate(printRunnable("스레드 테스트"), 1, 10, TimeUnit.SECONDS);
}

private static Runnable printRunnable(String message) {
    return () -> System.out.println(message + " (" + Thread.currentThread().getName() + ", " + LocalDateTime.now() + ") ");
}

[실행 결과]

예시 2