본문 바로가기
BackEnd/JAVA

[JAVA] Thread pool 을 위한 ExecutorService 의 생성과 submit, shutdown

by 성은2 2024. 11. 13.

Thread pool

Thread pool은 작업 처리에 사용되는 스레드를 제한된 개수만큼 정해 놓고 작업 큐(Queue)에 들어오는 작업들을 하나씩 스레드가 맡아 처리하는 것을 말한다.

=> 자원 효율성, 작업 처리 속도 향상(대기 중인 쓰레드 활용), 작업 제어(:동시 처리 가능한 개수 미리 지정)

Java에서는 Thread pool 을 구현하기 위해 Executor 및 ExecutorService 인터페이스를 사용한다.

 

ExecutorService 

ExecutorService는 비동기 모드에서 작업 실행을 간소화하는 JDK API입니다. 일반적으로 ExecutorService는 스레드 풀과 작업 (Runnable, Callable) 할당을 위한 API를 제공합니다.


Runnable  : 결과를 반환받을 수 없는 단일 run() 메서드가 있다.

Callable : Runnable의 발전된 형태로, 결과를 받을 수 있다.

public class RunnableSample implements Runnable {
    @Override
    public void run() {
        // 스레드로 실행할 코드
        System.out.println("rannable 의 run 메서드 실행");
    }
}

 

public class RunnableTest {
    public static void main(String[] args) {
        RunnableSample runnable = new RunnableSample();
        Thread thread = new Thread(runnable);
        thread.start();
    }
}

=> Runnable 인터페이스를 사용함으로 인해...

1. 비즈니스 로직과 스레드의 구분

  • Runnable 인터페이스: 로직을 담고 있는 클래스는 스레드 실행에만 필요한 코드를 Runnable로 구현하고, 실행은 Thread 객체가 담당합니다.

2. 코드 재사용성

  • 하나의 Runnable 객체를 여러 Thread에서 사용 가능합니다.

3. 다중 상속 제한의 한계 극복

  • 다른 클래스를 상속받고 있는 경우: 자바의 다중 상속 제한 때문에 Thread 클래스를 상속받을 수 없을 때 Runnable을 사용합니다.

4. 메모리 효율성

  • Thread 클래스를 상속받으면 각각의 스레드는 별도의 Thread 인스턴스를 생성하지만, Runnable 인터페이스를 구현함으로 여러 스레드가 같은 Runnable 객체를 공유할 수 있습니다.

 

ExecutorService 인스턴스화

1.1. Factory Methods of the Executors Class

Executor, ExecutorService는 모두 쓰레드 풀을 위한 인터페이스입니다.

직접 쓰레드를 다루는 것은 번거로우므로, 이를 도와주는 팩토리 클래스인 Executors가 등장하게 되었습니다.

보다 쉽게 ExecutorService 를 만들기 위해서는 Executors 클래스의 팩터리 메소드 중 하나인 newFixedThreadPool() 를 사용합니다.

ExecutorService executorService = Executors.newFixedThreadPool(10); // 쓰레드의 개수 지정 가능

 

1.2. Assigning Tasks to the ExecutorService

Executor인터페이스에서 상속되는 execute()와 submit(), invokeAny() 및 invokeAll()을 포함한 여러 방법을 사용하여 작업을 ExecutorService에 할당할 수 있다.

executorService.execute(runnableTask);
Future<String> future = executorService.submit(callableTask);

 

 

1.3 Shutting Down an ExecutorService

작업이 없어도 ExecutorService 는 자동적으로 파괴되지 않고, 새로운 일을 하기 위해 계속 살아있는 상태다.

이를 종료하는 구문을 작성해줘야 하는데,

ExecutorService를 제대로 종료하려면 shutdown() API와 shutdownNow() API를 사용해준다.

 

하지만 shutdown() 메서드가 ExecutorService를 즉시 파괴하지는 않는다.

이렇게 하면 실행 중인 모든 스레드가 현재 작업을 완료한 후 ExecutorService가 새 작업 수락을 중단하고 종료됨.

executorService.shutdown();

 

shutdownNow() 메서드는 실행자 서비스를 즉시 파괴하려고 하지만 실행 중인 모든 스레드가 동시에 중단된다는 보장은 없다.

List<Runnable> notExecutedTasks = executorService.shutDownNow();

이 방법은 처리 대기 중인 작업 목록을 반환함. 이러한 작업을 어떻게 처리할지는 개발자의 결정에 달려 있음.

 


오라클에서도 권장하는 실행자 서비스를 종료하는 좋은 방법 중 하나는 다음 두 가지 방법을 모두 awaitTermination()  메서드와 결합하여 사용하는 것이다:

executorService.shutdown(); // 중단
try {
    if (!executorService.awaitTermination(800, TimeUnit.MILLISECONDS)) { // 일정기간 기다림
        executorService.shutdownNow(); // 중단
    } 
} catch (InterruptedException e) {
    executorService.shutdownNow();
}

이 접근 방식을 사용하면 ExecutorService 는 먼저 새 작업 수행을 중단한 다음 모든 작업이 완료될 때까지 지정된 기간까지 기다립니다. 해당 기간이 만료되면 실행이 즉시 중단됩니다.

 

 

 

 

 

 

https://www.baeldung.com/java-executor-service-tutorial

https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/ExecutorService.html