Executors 클래스 의 newFixedThreadPool 메서드 는 고정된 수의 스레드로 executorService를 생성합니다 . newSingleThreadExecutor 메서드 와 달리 풀에서 원하는 스레드 수를 지정합니다. 내부적으로는 다음 코드가 호출됩니다.

new ThreadPoolExecutor(nThreads, nThreads,
                                      	0L, TimeUnit.MILLISECONDS,
                                      	new LinkedBlockingQueue());

corePoolSize ( 실행기 서비스가 시작될 때 준비(시작)될 스레드 수 ) 및 maximumPoolSize ( 실행기 서비스가 생성할 수 있는 최대 스레드 수 ) 매개변수는 동일한 값을 받습니다. newFixedThreadPool (nThreads ) . 그리고 정확히 같은 방식으로 우리 자신의 ThreadFactory 구현을 전달할 수 있습니다.

자, ExecutorService 가 필요한 이유를 살펴보겠습니다 .

고정된 수(n)의 스레드가 있는 ExecutorService 의 논리는 다음과 같습니다 .

  • 작업 처리를 위해 최대 n개의 스레드가 활성화됩니다.
  • n개 이상의 작업이 제출되면 스레드가 해제될 때까지 대기열에 보관됩니다.
  • 스레드 중 하나가 실패하고 종료되면 그 자리를 대신할 새 스레드가 생성됩니다.
  • 풀의 모든 스레드는 풀이 종료될 때까지 활성 상태입니다.

예를 들어 공항에서 보안 검색대를 통과하기 위해 기다리는 것을 상상해 보십시오. 보안 검색 직전까지 모든 사람이 한 줄에 서 있으며 승객은 모든 작업 검문소에 분산됩니다. 체크포인트 중 하나에서 지연이 발생하면 첫 번째 체크포인트가 비워질 때까지 두 번째 체크포인트만 대기열을 처리합니다. 그리고 하나의 검문소가 완전히 닫히면 다른 검문소가 열리면서 이를 대체하고 승객은 두 검문소를 통해 계속 처리됩니다.

우리는 조건이 이상적이라 할지라도 — 약속된 n개의 스레드가 안정적으로 작동하고, 오류로 끝나는 스레드는 항상 교체됨(제한된 리소스로 인해 실제 공항에서는 달성할 수 없음) — 시스템에 여전히 여러 개의 스레드가 작업을 처리할 수 있는 것보다 대기열이 더 빨리 증가하더라도 어떤 상황에서도 더 많은 스레드가 있을 수 없기 때문입니다.

ExecutorService가 고정된 수의 스레드에서 작동하는 방식을 실질적으로 이해하는 것이 좋습니다 . Runnable을 구현하는 클래스를 만들어 봅시다 . 이 클래스의 개체는 ExecutorService 에 대한 작업을 나타냅니다 .

public class Task implements Runnable {
    int taskNumber;

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

    @Override
    public void run() {
try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Processed user request #" + taskNumber + " on thread " + Thread.currentThread().getName());
    }
}

run() 메서드 에서 우리는 2초 동안 스레드를 차단하고 일부 워크로드를 시뮬레이션한 다음 현재 작업의 번호와 작업을 실행하는 스레드의 이름을 표시합니다.

ExecutorService executorService = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 30; i++) {
            executorService.execute(new Task(i));
        }

        executorService.shutdown();

먼저 기본 메서드에서 ExecutorService를 만들고 실행을 위해 30개의 작업을 제출합니다.

pool-1-thread-2 스레드에서 처리된 사용자 요청 #1
pool-1-thread-1 스레드에서 처리
된 사용자 요청 #0 pool-1-thread-3 스레드에서 처리된 사용자 요청 #2
pool-에서 처리된 사용자 요청 #5 1-thread-3 스레드
pool-1-thread-2 스레드에서 처리된 사용자 요청 #3
pool-1-thread-1 스레드에서 처리된 사용자 요청 #4 pool-1
-thread-1 스레드에서 처리된 사용자 요청 #8
처리된 사용자 pool-1-thread-3 스레드에서 요청 #6 pool
-1-thread-2 스레드에서 처리된 사용자 요청 #7 pool-1-thread
-3 스레드에서 처리된 사용자 요청 #10
pool-1-에서 처리된 사용자 요청 #9 thread-1 스레드
pool-1-thread-2 스레드에서 처리된 사용자 요청 #11
pool-1-thread-3 스레드에서 처리된 사용자 요청 #12
pool-1-thread-2 스레드에서 처리된 사용자 요청 #14
pool-1-thread-1 스레드에서 처리된 사용자 요청 #13 pool
-1-thread-3 스레드에서 처리된 사용자 요청 #15
pool-에서 처리된 사용자 요청 #16 1-thread-2 스레드
pool-1-thread-1 스레드에서 처리된 사용자 요청 #17
pool-1-thread-3 스레드에서 처리된 사용자 요청 #18
pool-1-thread-2 스레드에서 처리된 사용자 요청 #19
처리된 사용자 pool-1-thread-1 스레드에서
요청 #20 pool-1-thread-3 스레드에서 처리된 사용자 요청 #21 pool-
1-thread-2 스레드에서 처리된 사용자 요청 #22 pool-1-
에서 처리된 사용자 요청 #23 thread-1 스레드
pool-1-thread-2 스레드에서 처리된 사용자 요청 #25
pool-1-thread-3 스레드에서 처리된 사용자 요청 #24
pool-1-thread-1 스레드에서 처리된 사용자 요청 #26
pool-1-thread-2 스레드에서 처리된 사용자 요청 #27 pool-
1-thread-3 스레드에서 처리된 사용자 요청 #28 pool-1-thread-3 스레드에서
처리된 사용자 요청 #29 1-스레드-1 스레드

콘솔 출력은 작업이 이전 작업에 의해 해제된 후 다른 스레드에서 실행되는 방법을 보여줍니다.

이제 작업 수를 100개로 늘리고 100개 작업을 제출한 후 awaitTermination (11, SECONDS) 메서드를 호출합니다. 숫자와 시간 단위를 인수로 전달합니다. 이 방법은 메인 스레드를 11초 동안 차단합니다. 그런 다음 shutdownNow()를 호출하여 모든 작업이 완료될 때까지 기다리지 않고 ExecutorService를 강제 로 종료합니다.

ExecutorService executorService = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 100; i++) {
            executorService.execute(new Task(i));
        }

        executorService.awaitTermination(11, SECONDS);

        executorService.shutdownNow();
        System.out.println(executorService);

마지막에는 executorService 의 상태에 대한 정보를 표시합니다 .

우리가 얻는 콘솔 출력은 다음과 같습니다.

pool-1-thread-1 스레드에서 처리된 사용자 요청 #
0 pool-1-thread-3 스레드에서
처리된 사용자 요청 #2 pool-1-thread-2 스레드에서 처리된 사용자 요청 #1 풀-1-thread-2 스레드에서
처리된 사용자 요청 #4 1-thread-3 스레드
pool-1-thread-2 스레드에서 처리된 사용자 요청 #5
pool-1-thread-1 스레드에서 처리된 사용자 요청 #3
pool-1-thread-3 스레드에서 처리된 사용자 요청 #6
처리된 사용자 pool-1-thread-2 스레드에서 요청 #7 pool-
1-thread-1 스레드에서 처리된 사용자 요청 #8 pool-1
-thread-3 스레드에서 처리된 사용자 요청 #9
pool-1-에서 처리된 사용자 요청 #11 thread-1 스레드
pool-1-thread-2 스레드에서 처리된 사용자 요청 #10
pool-1-thread-1 스레드에서 처리된 사용자 요청 #13
pool-1-thread-2 스레드에서 처리된 사용자 요청 #14
pool-1-thread-3 스레드에서 처리된 사용자 요청 #12
java.util.concurrent.ThreadPoolExecutor@452b3a41[종료 중, 풀 크기 = 3, 활성 스레드 = 3 , 대기 중인 작업 = 0, 완료된 작업 = 15]
pool-1-thread-3 스레드에서 처리된 사용자 요청 #17 pool-1
-thread-1 스레드에서 처리된 사용자 요청 #15
pool-1-thread에서 처리된 사용자 요청 #16 -2 스레드

그 다음에는 3개의 활성 작업에서 절전 메서드 에 의해 발생한 3개의 InterruptedExceptions가 뒤따릅니다 .

프로그램이 종료되면 15개의 작업이 완료되지만 풀에는 여전히 작업 실행을 완료하지 않은 3개의 활성 스레드가 있음을 알 수 있습니다. 이 세 스레드에서 interrupt () 메서드가 호출됩니다. 즉, 작업이 완료되지만 우리의 경우 sleep 메서드는 InterruptedException 을 발생시킵니다 . 또한 shutdownNow() 메서드가 호출된 후 작업 대기열이 지워지는 것을 볼 수 있습니다.

따라서 풀에서 고정된 수의 스레드와 함께 ExecutorService를 사용할 때 작동 방식을 기억해야 합니다. 이 유형은 일정한 부하가 알려진 작업에 적합합니다.

여기에 또 다른 흥미로운 질문이 있습니다. 단일 스레드에 대해 실행기를 사용해야 하는 경우 어떤 메서드를 호출해야 합니까? newSingleThreadExecutor() 또는 newFixedThreadPool(1) ?

두 실행기 모두 동일한 동작을 합니다. 유일한 차이점은 newSingleThreadExecutor() 메서드가 나중에 추가 스레드를 사용하도록 재구성할 수 없는 실행기를 반환한다는 것입니다.