다른 유형의 스레드 풀은 "캐시"입니다. 이러한 스레드 풀은 고정 스레드 풀만큼 일반적으로 사용됩니다.

이름에서 알 수 있듯이 이러한 종류의 스레드 풀은 스레드를 캐시합니다. 사용하지 않는 스레드를 제한된 시간 동안 활성 상태로 유지하여 해당 스레드를 재사용하여 새 작업을 수행합니다. 이러한 스레드 풀은 합리적인 양의 가벼운 작업이 있을 때 가장 적합합니다.

"합리적인 금액"의 의미는 다소 광범위하지만 이러한 풀이 모든 작업 수에 적합하지 않다는 점을 알아야 합니다. 예를 들어 백만 개의 작업을 만들고 싶다고 가정합니다. 각 작업에 아주 적은 시간이 걸리더라도 여전히 불합리한 양의 리소스를 사용하고 성능을 저하시킵니다. 예를 들어 I/O 작업과 같이 실행 시간을 예측할 수 없는 경우에도 이러한 풀을 피해야 합니다.

내부적으로 ThreadPoolExecutor 생성자는 다음 인수를 사용하여 호출됩니다.

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
      new SynchronousQueue<Runnable>());
}

다음 값은 생성자에 인수로 전달됩니다.

모수
corePoolSize ( 실행자 서비스가 시작될 때 준비(시작)될 스레드 수 ) 0
maximumPoolSize ( 실행기 서비스가 생성할 수 있는 최대 스레드 수 ) 정수.MAX_VALUE
keepAliveTime (스레드 수가 corePoolSize 보다 큰 경우 해제된 스레드가 소멸되기 전에 계속 유지되는 시간 ) 60L
단위 (시간 단위) TimeUnit.SECONDS
workQueue (대기열 구현) 새로운 SynchronousQueue<실행 가능>()

그리고 정확히 같은 방식으로 우리 자신의 ThreadFactory 구현을 전달할 수 있습니다.

SynchronousQueue에 대해 알아보겠습니다.

동기식 전송의 기본 아이디어는 매우 간단하지만 직관에 반합니다(즉, 직감이나 상식으로 볼 그것이 틀렸다는 것을 알 수 있습니다). 같은 시간. 즉, 새 작업이 도착하자마자 실행 중인 스레드가 이미 작업을 선택했기 때문에 동기 대기열에는 작업이 있을 수 없습니다 .

새 작업이 대기열에 들어올 때 풀에 사용 가능한 활성 스레드가 있으면 작업을 선택합니다. 모든 스레드가 사용 중이면 새 스레드가 생성됩니다.

캐시된 풀은 0개의 스레드로 시작하여 잠재적으로 Integer.MAX_VALUE 스레드로 확장될 수 있습니다. 기본적으로 캐시된 스레드 풀의 크기는 시스템 리소스에 의해서만 제한됩니다.

시스템 리소스를 절약하기 위해 캐시된 스레드 풀은 1분 동안 유휴 상태인 스레드를 제거합니다.

실제로 어떻게 작동하는지 봅시다. 사용자 요청을 모델링하는 태스크 클래스를 생성합니다.

public class Task implements Runnable {
   int taskNumber;

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

   @Override
   public void run() {
       System.out.println("Processed user request #" + taskNumber + " on thread " + Thread.currentThread().getName());
   }
}

기본 메서드 에서 newCachedThreadPool을 생성한 다음 실행을 위해 3개의 작업을 추가합니다. 여기서 서비스 상태를 인쇄합니다 (1) .

다음으로 30초 동안 일시 중지하고 다른 작업을 시작하고 상태 (2) 를 표시합니다 .

그런 다음 메인 스레드를 70초 동안 일시 중지하고 상태 (3)를 인쇄한 다음 다시 3개의 작업을 추가하고 상태 (4) 를 다시 인쇄합니다 .

작업을 추가한 직후에 상태를 표시하는 위치에서 최신 출력을 위해 먼저 1초 절전을 추가합니다.

ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 3; i++) {
            executorService.submit(new Task(i));
        }

        TimeUnit.SECONDS.sleep(1);
            System.out.println(executorService);	//(1)

        TimeUnit.SECONDS.sleep(30);

        executorService.submit(new Task(3));
        TimeUnit.SECONDS.sleep(1);
            System.out.println(executorService);	//(2)

        TimeUnit.SECONDS.sleep(70);

            System.out.println(executorService);	//(3)

        for (int i = 4; i < 7; i++) {
            executorService.submit(new Task(i));
        }

        TimeUnit.SECONDS.sleep(1);
            System.out.println(executorService);	//(4)
        executorService.shutdown();

결과는 다음과 같습니다.

pool-1-thread-1 스레드에서 처리된 사용자 요청 #0
pool-1-thread-2 스레드에서 처리된 사용자 요청
#1 pool-1-thread-3 스레드에서 처리된 사용자 요청 #2
(1) java.util.concurrent .ThreadPoolExecutor@f6f4d33[실행 중, 풀 크기 = 3, 활성 스레드 = 0, 대기 중인 작업 = 0, 완료된 작업 = 3]
pool-1-thread-2 스레드
(2) java.util.concurrent에서 처리된 사용자 요청 #3. ThreadPoolExecutor@f6f4d33[실행 중, 풀 크기 = 3, 활성 스레드 = 0, 대기 중인 작업 = 0, 완료된 작업 = 4] (3)
java.util.concurrent.ThreadPoolExecutor@f6f4d33[실행 중, 풀 크기 = 0, 활성 스레드 = 0 , 대기 중인 작업 = 0, 완료된 작업 = 4]
pool-1-thread-4 스레드에서 처리된 사용자 요청 #4
pool-1-thread-5 스레드에서 처리된 사용자 요청 #5
pool-1-thread-4 스레드
(4)에서 처리된 사용자 요청 #6 java.util.concurrent.ThreadPoolExecutor@f6f4d33[실행 중, 풀 크기 = 2, 활성 스레드 = 0, 대기 중인 작업 = 0, 완료된 작업 = 7]

각 단계를 살펴보겠습니다.

단계 설명
1(작업 3개 완료 후) 우리는 3개의 스레드를 생성했고 이 3개의 스레드에서 3개의 작업이 실행되었습니다.
상태가 표시되면 3가지 작업이 모두 완료되고 스레드가 다른 작업을 수행할 준비가 된 것입니다.
2(30초 일시 중지 후 다른 작업 실행) 30초 동안 활동이 없으면 스레드는 여전히 활성 상태이며 작업을 기다립니다.
나머지 라이브 스레드 풀에서 가져온 스레드에서 다른 작업이 추가되고 실행됩니다.
풀에 추가된 새 스레드가 없습니다.
3(70초 일시 중지 후) 스레드가 풀에서 제거되었습니다.
작업을 수락할 준비가 된 스레드가 없습니다.
4(3개의 작업을 더 실행한 후) 더 많은 작업이 수신된 후 새 스레드가 생성되었습니다. 이번에는 두 개의 스레드만 3개의 작업을 처리했습니다.

이제 다른 유형의 실행자 서비스 논리에 익숙해졌습니다.

Executors 유틸리티 클래스 의 다른 메서드와 유사하게 newCachedThreadPool 메서드에는 ThreadFactory 개체를 인수로 사용하는 오버로드된 버전도 있습니다 .