1 스레드에 ExecutorService가 필요한 이유는 무엇입니까?

Executors.newSingleThreadExecutor 메서드를 사용하여 단일 스레드를 포함하는 풀이 있는 ExecutorService를 만들 수 있습니다. 풀의 논리는 다음과 같습니다.

  • 서비스는 한 번에 하나의 작업만 실행합니다.
  • 실행을 위해 N개의 작업을 제출하면 모든 N개의 작업이 단일 스레드에 의해 차례로 실행됩니다.
  • 스레드가 중단되면 나머지 작업을 실행하기 위해 새 스레드가 생성됩니다.

프로그램에 다음 기능이 필요한 상황을 상상해 봅시다.

30초 이내에 사용자 요청을 처리해야 하지만 단위 시간당 하나의 요청만 처리해야 합니다.

사용자 요청을 처리하기 위한 작업 클래스를 만듭니다.


class Task implements Runnable {
   private final int taskNumber;

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

   @Override
   public void run() {
       try {
           Thread.sleep(1000);
       } catch (InterruptedException ignored) {
       }
       System.out.printf("Processed request #%d on thread id=%d\\n", taskNumber, Thread.currentThread().getId());
   }
}
    

이 클래스는 들어오는 요청을 처리하는 동작을 모델링하고 해당 번호를 표시합니다.

다음으로, 기본 메서드에서 들어오는 요청을 순차적으로 처리하는 데 사용할 스레드 1개에 대한 ExecutorService를 만듭니다 . 작업 조건이 "30초 이내"로 규정되어 있으므로 30초 대기 시간을 추가한 다음 ExecutorService를 강제로 중지 합니다 .


public static void main(String[] args) throws InterruptedException {
   ExecutorService executorService = Executors.newSingleThreadExecutor();

   for (int i = 0; i < 1_000; i++) {
       executorService.execute(new Task(i));
   }
   executorService.awaitTermination(30, TimeUnit.SECONDS);
   executorService.shutdownNow();
}
    

프로그램을 시작하면 콘솔에 요청 처리에 대한 메시지가 표시됩니다.

스레드 id=16에서 처리된 요청 #0
스레드 id=16에서 처리된 요청 #1 스레드
id=16에서 처리된 요청 #2

스레드 id=16에서 처리된 요청 #29

30초 동안 요청을 처리한 후 executorService는 현재 작업(실행 중인 작업)을 중지하고 보류 중인 모든 작업을 취소하는 shutdownNow() 메서드를 호출합니다 . 그 후 프로그램이 성공적으로 종료됩니다.

그러나 모든 것이 항상 완벽한 것은 아닙니다. 우리 프로그램은 풀의 유일한 스레드가 선택한 작업 중 하나가 잘못 작동하고 심지어 스레드를 종료하는 상황을 쉽게 가질 수 있기 때문입니다. 이 상황을 시뮬레이트하여 이 경우 executorService가 단일 스레드로 작동하는 방식을 파악할 수 있습니다 .

이를 위해 작업 중 하나가 실행되는 동안 안전하지 않고 사용되지 않는 Thread.currentThread().stop() 메서드를 사용하여 스레드를 종료합니다. 작업 중 하나가 스레드를 종료하는 상황을 시뮬레이트하기 위해 의도적으로 이 작업을 수행하고 있습니다.

Task 클래스 에서 run 메서드를 변경합니다 .


@Override
public void run() {
   try {
       Thread.sleep(1000);
   } catch (InterruptedException ignored) {
   }

   if (taskNumber == 5) {
       Thread.currentThread().stop();
   }

   System.out.printf("Processed request #%d on thread id=%d\\n", taskNumber, Thread.currentThread().getId());
}
    

작업 #5를 중단하겠습니다.

작업 #5의 끝에서 스레드가 중단된 출력이 어떻게 보이는지 봅시다.

스레드 id=16에서 처리된 요청 #0 스레드
id=16에서 처리된 요청 #1 스레드 id=16에서
처리된 요청 #2 스레드 id=16
에서 처리된 요청 #3 스레드 id=16
에서 처리된 요청 #4 스레드 id=16에서
처리된 요청 #6 thread id=17
thread id=17에서 처리된 요청 #7

thread id=17에서 처리된 요청 #29

작업 5의 끝에서 스레드가 중단된 후 작업은 이전에 식별자가 16인 스레드에서 실행되었지만 식별자가 17인 스레드에서 실행되기 시작합니다. 단일 스레드, 이것은 단지 한 가지를 의미할 수 있습니다. executorService는 중지된 스레드를 새 스레드로 교체하고 작업을 계속 실행했습니다.

따라서 작업을 한 번에 하나씩 순차적으로 처리하고 이전 작업의 완료와 관계없이 대기열에서 작업을 계속 처리하려는 경우 단일 스레드 풀과 함께 newSingleThreadExecutor를 사용해야 합니다 (예: 스레드를 죽입니다).

스레드팩토리

스레드 생성 및 재생성에 대해 말할 때 언급하지 않을 수 없습니다.스레드팩토리.

스레드팩토리요청 시 새 스레드를 생성하는 개체입니다.

자체 스레드 생성 팩토리를 생성하고 인스턴스를 Executors.newSingleThreadExecutor(ThreadFactory threadFactory) 메서드에 전달할 수 있습니다.


ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "MyThread");
            }
        });
                    
새 스레드를 생성하는 메서드를 재정의하고 스레드 이름을 생성자에 전달합니다.

ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r, "MyThread");
                thread.setPriority(Thread.MAX_PRIORITY);
                return thread;
            }
        });
                    
생성된 스레드의 이름과 우선 순위를 변경했습니다.

따라서 2개의 오버로드된 Executors.newSingleThreadExecutor 메서드가 있음을 알 수 있습니다 . 하나는 매개변수가 없고 다른 하나는 ThreadFactory 매개변수가 있습니다.

ThreadFactory 를 사용하면 필요에 따라 생성된 스레드를 구성할 수 있습니다(예: 우선 순위 설정, 스레드 하위 클래스 사용, 스레드에 UncaughtExceptionHandler 추가 등).