1. 개요

ExecutorService 는 비동기 모드에서 작업 실행을 단순화하는 JDK API입니다. 일반적으로 ExecutorService는 작업 할당을 위한 스레드 풀과 API를 자동으로 제공합니다.

2. ExecutorService 인스턴스화

2.1. 실행자 클래스 의 팩토리 메서드

ExecutorService를 만드는 가장 쉬운 방법은 Executors 클래스 의 팩터리 메서드 중 하나를 사용하는 것입니다 .

예를 들어 다음 코드 줄은 10개의 스레드가 있는 스레드 풀을 만듭니다.

ExecutorService executor = Executors.newFixedThreadPool(10);

특정 사용 사례를 충족하는 미리 정의된 ExecutorService를 만드는 몇 가지 다른 팩터리 메서드가 있습니다 . 요구 사항에 가장 적합한 방법을 찾으려면 Oracle의 공식 문서를 참조하십시오 .

2.2. ExecutorService 직접 생성

ExecutorService 는 인터페이스 이므로 해당 구현의 인스턴스를 사용할 수 있습니다. java.util.concurrent 패키지 에서 선택할 수 있는 여러 구현이 있거나 직접 만들 수 있습니다.

예를 들어 ThreadPoolExecutor 클래스에는 실행기 서비스 및 해당 내부 풀을 구성하는 데 사용할 수 있는 몇 가지 생성자가 있습니다.

ExecutorService executorService = 
  new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,   
  new LinkedBlockingQueue<Runnable>());

위의 코드가 팩토리 메서드 newSingleThreadExecutor()의 소스 코드 와 매우 유사하다는 것을 알 수 있습니다 . 대부분의 경우 자세한 수동 구성이 필요하지 않습니다.

3. ExecutorService 에 작업 할당

ExecutorService는 RunnableCallable 작업을 실행할 수 있습니다 . 이 기사에서는 일을 단순하게 유지하기 위해 두 가지 기본 작업이 사용됩니다. 여기에서는 익명 내부 클래스 대신 람다 식을 사용합니다.

Runnable runnableTask = () -> {
    try {
        TimeUnit.MILLISECONDS.sleep(300);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};

Callable<String> callableTask = () -> {
    TimeUnit.MILLISECONDS.sleep(300);
    return "Task's execution";
};

List<Callable<String>> callableTasks = new ArrayList<>();
callableTasks.add(callableTask);
callableTasks.add(callableTask);
callableTasks.add(callableTask);

Executor 인터페이스 에서 상속된 execute()submit() , invokeAny()  및 invokeAll() 을 비롯한 여러 메서드를 사용하여 ExecutorService 에 작업을 할당할 수 있습니다 .

execute () 메서드는 무효 이며 작업 실행 결과를 얻거나 작업 상태(실행 중인지)를 확인할 수 있는 가능성을 제공하지 않습니다.

executorService.execute(runnableTask);

submit() 은 Callable 또는 Runnable 작업을 ExecutorService제출 하고 Future 유형의 결과를 반환합니다.

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

invokeAny() 는 ExecutorService 에 작업 모음을 할당하여각 작업을 실행하고 한 작업의 성공적인 실행 결과를 반환합니다(성공적인 실행이 있는 경우).

String result = executorService.invokeAny(callableTasks);

invokeAll() 은 ExecutorService 에 작업 모음을 할당하여 각 작업을 실행하고 모든 작업 실행 결과를 Future 유형의 객체 List 형식으로 반환합니다.

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

계속 진행하기 전에 ExecutorService를 종료하고 Future 반환 유형을 처리하는 두 가지 항목을 더 논의해야 합니다 .

4. ExecutorService 종료

일반적으로 ExecutorService는 처리할 작업이 없을 때 자동으로 소멸되지 않습니다. 그것은 살아 있고 새로운 일을 할 때까지 기다릴 것입니다.

어떤 경우에는 앱이 불규칙적으로 나타나는 작업을 처리해야 하거나 컴파일 시간에 작업 수량을 알 수 없는 경우와 같이 매우 유용합니다.

반면 대기중인 ExecutorService 로 인해 JVM이 계속 실행되기 때문에 앱이 끝에 도달했지만 중지되지 않을 수 있습니다.

ExecutorService 를 제대로 종료하기 위해 shutdown()shutdownNow() API가 있습니다 .

shutdown () 메서드는 ExecutorService 를 즉시 파괴하지 않습니다 . ExecutorService 가 새 작업 수락을 중지하고 실행 중인 모든 스레드가 현재 작업을 완료한 후 종료 됩니다 .

executorService.shutdown();

shutdownNow () 메서드는 ExecutorService를 즉시 삭제하려고 시도 하지만 실행 중인 모든 스레드가 동시에 중지된다는 보장은 없습니다.

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

이 메서드는 처리 대기 중인 작업 List을 반환합니다. 이러한 작업을 어떻게 처리할지 결정하는 것은 개발자의 몫입니다.

ExecutorService ( Oracle에서도 권장 ) 를 종료하는 한 가지 좋은 방법은 awaitTermination() 메서드 와 함께 이 두 메서드를 모두 사용하는 것입니다 .

executorService.shutdown();
try {
    if (!executorService.awaitTermination(800, TimeUnit.MILLISECONDS)) {
        executorService.shutdownNow();
    } 
} catch (InterruptedException e) {
    executorService.shutdownNow();
}

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

5. 미래의 인터페이스

submit ()invokeAll() 메서드는 객체 또는 Future 유형의 객체 컬렉션을 반환하여 작업 실행 결과를 얻거나 작업 상태(실행 중인지)를 확인할 수 있습니다.

Future 인터페이스 Runnable 작업 의 경우 Callable 작업 실행 또는 null 의 실제 결과를 반환하는 특수 차단 메서드 get() 을 제공합니다.

Future<String> future = executorService.submit(callableTask);
String result = null;
try {
    result = future.get();
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

작업이 실행 중인 동안 get() 메서드를 호출하면 작업이 제대로 실행되고 결과를 사용할 수 있을 때까지 실행이 차단됩니다.

get() 메서드 로 인한 매우 긴 차단으로 인해 애플리케이션의 성능이 저하될 수 있습니다. 결과 데이터가 중요하지 않은 경우 시간 제한을 사용하여 이러한 문제를 방지할 수 있습니다.

String result = future.get(200, TimeUnit.MILLISECONDS);

실행 기간이 지정된 것보다 길면(이 경우 200밀리초) TimeoutException 이 발생합니다.

isDone() 메서드를 사용하여 할당된 작업이 이미 처리되었는지 여부를 확인할 수 있습니다.

Future 인터페이스 는 또한 cancel() 메소드 를 사용하여 작업 실행을 취소 하고 isCancelled() 메소드를 사용하여 취소를 확인합니다 .

boolean canceled = future.cancel(true);
boolean isCancelled = future.isCancelled();

6. ScheduledExecutorService 인터페이스

ScheduledExecutorService는 미리 정의된 지연 후 및/또는 주기적으로 작업을 실행합니다.

다시 한 번 ScheduledExecutorService 를 인스턴스화하는 가장 좋은 방법은 Executors 클래스 의 팩토리 메서드를 사용하는 것입니다 .

이 섹션에서는 하나의 스레드와 함께 ScheduledExecutorService를 사용합니다 .

ScheduledExecutorService executorService = Executors
  .newSingleThreadScheduledExecutor();

고정 지연 후 단일 작업 실행을 예약하려면 ScheduledExecutorServicescheduled() 메서드를 사용합니다 .

두 개의 scheduled() 메서드를 사용하여 Runnable 또는 Callable 작업을 실행할 수 있습니다 .

Future<String> resultFuture = 
  executorService.schedule(callableTask, 1, TimeUnit.SECONDS);

scheduleAtFixedRate () 메서드를 사용하면 고정 지연 후 주기적으로 작업을 실행할 수 있습니다. 위의 코드는 callableTask 를 실행하기 전에 1초 동안 지연됩니다 .

다음 코드 블록은 100밀리초의 초기 지연 후에 작업을 실행합니다. 그런 다음 450밀리초마다 동일한 작업을 실행합니다.

executorService.scheduleAtFixedRate(runnableTask, 100, 450, TimeUnit.MILLISECONDS);

프로세서가 할당된 작업을 실행하는 데 scheduleAtFixedRate() 메서드 의 기간 매개 변수 보다 더 많은 시간이 필요한 경우 ScheduledExecutorService는 다음 작업을 시작하기 전에 현재 작업이 완료될 때까지 대기합니다.

작업 반복 사이에 고정 길이 지연이 필요한 경우 scheduleWithFixedDelay()를 사용해야 합니다.

예를 들어 다음 코드는 현재 실행의 끝과 다른 실행의 시작 사이에 150밀리초의 일시 중지를 보장합니다.

executorService.scheduleWithFixedDelay(task, 100, 150, TimeUnit.MILLISECONDS);

scheduleAtFixedRate()scheduleWithFixedDelay() 메서드 계약 에 따라 작업의 기간 실행은 ExecutorService가 종료되거나 작업 실행 중에 예외가 발생하는 경우 종료됩니다 .

7. ExecutorService VS 포크/조인

Java 7 릴리스 이후 많은 개발자는 ExecutorService 프레임워크를 fork/join 프레임워크로 교체하기로 결정했습니다.

그러나 이것이 항상 올바른 결정은 아닙니다. 포크/조인과 관련된 단순성과 빈번한 성능 향상에도 불구하고 동시 실행에 대한 개발자 제어가 줄어듭니다.

ExecutorService는 개발자에게 생성된 스레드 수와 별도의 스레드에서 실행해야 하는 작업의 세분성을 제어할 수 있는 기능을 제공합니다. ExecutorService 의 가장 좋은 사용 사례는 "하나의 작업에 하나의 스레드" 체계에 따라 트랜잭션 또는 요청과 같은 독립적인 작업을 처리하는 것입니다.

대조적으로 오라클의 문서에 따르면 fork/join은 재귀적으로 더 작은 조각으로 나눌 수 있는 작업의 속도를 높이도록 설계되었습니다.

8. 결론

ExecutorService 의 상대적 단순성에도 불구하고 몇 가지 공통적인 함정이 있습니다.

요약하자면 다음과 같습니다.

사용하지 않는 ExecutorService를 활성 상태로 유지 : ExecutorService를 종료하는 방법에 대한 자세한 설명은 섹션 4를 참조하십시오 .

고정 길이 스레드 풀을 사용하는 동안 잘못된 스레드 풀 용량 : 응용 프로그램이 작업을 효율적으로 실행하는 데 필요한 스레드 수를 결정하는 것은 매우 중요합니다. 스레드 풀이 너무 크면 대부분 대기 모드에 있는 스레드를 만드는 데만 불필요한 오버헤드가 발생합니다. Queue에 있는 작업에 대한 대기 시간이 길기 때문에 너무 적으면 애플리케이션이 응답하지 않는 것처럼 보일 수 있습니다.

작업 취소 후 Futureget() 메서드 호출 : 이미 취소된 작업의 결과를 얻으려고 시도하면 CancellationException 이 트리거됩니다 .

Futureget() 메서드를 사용한 예기치 않게 긴 차단 : 예기치 않은 대기를 피하기 위해 시간 제한을 사용해야 합니다.

항상 그렇듯이 이 기사의 코드는 GitHub 리포지토리 에서 사용할 수 있습니다 .

res – REST with Spring (eBook) (everywhere)