기사 이해 및 Java 스레드 풀 이해--ThreadPoolExecutor 상세 설명

이해하기 위한 질문으로 먼저

  • 스레드 풀이 있는 이유는 무엇입니까?
  • Java에서 스레드 풀을 구현하고 관리하는 방법은 무엇입니까?사용 방법에 대한 간단한 예를 들어주십시오.
  • 많은 회사에서 Executor를 사용하여 스레드 풀을 만들 수 없는 이유는 무엇입니까?그럼 사용을 권장하는 방법은 무엇입니까?
  • ThreadPoolExecutor의 핵심 구성 매개변수는 무엇입니까?간단히 설명해주세요.
  • ThreadPoolExecutor가 생성할 수 있는 세 개의 스레드 풀은 무엇입니까?
  • 대기열이 가득 차고 작업자 수가 maxSize에 도달하면 어떻게 됩니까?
  • RejectedExecutionHandler 전략 ThreadPoolExecutor의 기본 전략은 무엇입니까?
  • 스레드 풀의 작업 실행 메커니즘에 대해 간략히 설명하자면 execute –> addWorker –>runworker(getTask)
  • 스레드 풀에서 작업은 어떻게 제출됩니까?
  • 스레드 풀의 작업은 어떻게 닫히나요?
  • 스레드 풀을 구성할 때 고려해야 할 구성 요소는 무엇입니까?
  • 스레드 풀의 상태를 모니터링하는 방법은 무엇입니까?

 스레드 풀이 있는 이유

스레드 풀은 스레드를 균일하게 할당, 조정 및 모니터링할 수 있습니다.

  • 리소스 소비 감소(스레드는 무기한 생성되었다가 사용 후 폐기됨)
  • 응답 속도 향상(스레드 생성 필요 없음)
  • 스레드 관리성 향상

# ThreadPoolExecutor 예제

Java는 스레드 풀을 어떻게 구현하고 관리합니까?

JDK 5부터는 작업 단위가 실행 메커니즘과 분리되어 작업 단위에 Runnable과 Callable이 포함되며 실행 메커니즘은 Executor 프레임워크에서 제공됩니다.

  • 작업자 스레드
public class WorkerThread implements Runnable {
     
    private String command;
     
    public WorkerThread(String s){
        this.command=s;
    }
 
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+" Start. Command = "+command);
        processCommand();
        System.out.println(Thread.currentThread().getName()+" End.");
    }
 
    private void processCommand() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
 
    @Override
    public String toString(){
        return this.command;
    }
}
  • SimpleThreadPool
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class SimpleThreadPool {
 
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            Runnable worker = new WorkerThread("" + i);
            executor.execute(worker);
          }
        executor.shutdown(); // This will make the executor accept no new threads and finish all existing threads in the queue
        while (!executor.isTerminated()) { // Wait until all threads are finish,and also you can use "executor.awaitTermination();" to wait
        }
        System.out.println("Finished all threads");
    }

}

프로그램에서 고정된 크기의 작업자 스레드 5개로 스레드 풀을 만들었습니다. 그런 다음 스레드 풀에 10개의 작업을 할당합니다. 스레드 풀 크기가 5이므로 5개의 작업자 스레드를 시작하여 5개의 작업을 먼저 처리하고 다른 작업은 대기 상태가 됩니다. 작업이 완료되면 작업자 스레드는 대기 중인 다른 작업이 실행되기를 기다립니다.

다음은 위 프로그램의 출력입니다.

pool-1-thread-2 Start. Command = 1
pool-1-thread-4 Start. Command = 3
pool-1-thread-1 Start. Command = 0
pool-1-thread-3 Start. Command = 2
pool-1-thread-5 Start. Command = 4
pool-1-thread-4 End.
pool-1-thread-5 End.
pool-1-thread-1 End.
pool-1-thread-3 End.
pool-1-thread-3 Start. Command = 8
pool-1-thread-2 End.
pool-1-thread-2 Start. Command = 9
pool-1-thread-1 Start. Command = 7
pool-1-thread-5 Start. Command = 6
pool-1-thread-4 Start. Command = 5
pool-1-thread-2 End.
pool-1-thread-4 End.
pool-1-thread-3 End.
pool-1-thread-5 End.
pool-1-thread-1 End.
Finished all threads

출력은 스레드 풀에서 처음부터 끝까지 "pool-1-thread-1"에서 "pool-1-thread-5"라는 이름의 스레드가 5개뿐이며 이 5개의 스레드는 작업이 완료되어도 죽지 않는다는 것을 보여줍니다. 항상 존재하며 스레드 풀이 죽을 때까지 스레드 풀에 할당된 작업 실행을 담당합니다.

Executors 클래스는 ThreadPoolExecutor를 사용하는 간단한 ExecutorService 구현을 제공하지만 ThreadPoolExecutor는 그 이상을 제공합니다. ThreadPoolExecutor 인스턴스를 생성할 때 활성 스레드 수를 지정할 수 있으며, 스레드 풀의 크기를 제한하고 작업 대기열에 맞지 않는 작업을 처리하기 위해 자체적으로 RejectedExecutionHandler 구현을 생성할 수도 있습니다.

다음은 사용자 지정 RejectedExecutionHandler 인터페이스의 구현입니다.

  • RejectedExecutionHandlerImpl.java
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
 
public class RejectedExecutionHandlerImpl implements RejectedExecutionHandler {
 
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        System.out.println(r.toString() + " is rejected");
    }
 
}

ThreadPoolExecutor는 실행기의 현재 상태, 스레드 풀 크기, 활성 스레드 수 및 작업 수를 쿼리하는 데 사용할 수 있는 메서드를 제공합니다. 그래서 모니터링 스레드를 사용하여 특정 시간 간격으로 실행기 정보를 인쇄합니다.

  • MyMonitorThread.java
import java.util.concurrent.ThreadPoolExecutor;
 
public class MyMonitorThread implements Runnable
{
    private ThreadPoolExecutor executor;
     
    private int seconds;
     
    private boolean run=true;
 
    public MyMonitorThread(ThreadPoolExecutor executor, int delay)
    {
        this.executor = executor;
        this.seconds=delay;
    }
     
    public void shutdown(){
        this.run=false;
    }
 
    @Override
    public void run()
    {
        while(run){
                System.out.println(
                    String.format("[monitor] [%d/%d] Active: %d, Completed: %d, Task: %d, isShutdown: %s, isTerminated: %s",
                        this.executor.getPoolSize(),
                        this.executor.getCorePoolSize(),
                        this.executor.getActiveCount(),
                        this.executor.getCompletedTaskCount(),
                        this.executor.getTaskCount(),
                        this.executor.isShutdown(),
                        this.executor.isTerminated()));
                try {
                    Thread.sleep(seconds*1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
        }
             
    }
}

다음은 ThreadPoolExecutor를 사용한 스레드 풀 구현의 예입니다.

  • WorkerPool.java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
 
public class WorkerPool {
 
    public static void main(String args[]) throws InterruptedException{
        //RejectedExecutionHandler implementation
        RejectedExecutionHandlerImpl rejectionHandler = new RejectedExecutionHandlerImpl();
        //Get the ThreadFactory implementation to use
        ThreadFactory threadFactory = Executors.defaultThreadFactory();
        //creating the ThreadPoolExecutor
        ThreadPoolExecutor executorPool = new ThreadPoolExecutor(2, 4, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2), threadFactory, rejectionHandler);
        //start the monitoring thread
        MyMonitorThread monitor = new MyMonitorThread(executorPool, 3);
        Thread monitorThread = new Thread(monitor);
        monitorThread.start();
        //submit work to the thread pool
        for(int i=0; i<10; i++){
            executorPool.execute(new WorkerThread("cmd"+i));
        }
         
        Thread.sleep(30000);
        //shut down the pool
        executorPool.shutdown();
        //shut down the monitor thread
        Thread.sleep(5000);
        monitor.shutdown();
         
    }
}

ThreadPoolExecutor를 초기화할 때 초기 풀 크기를 2로, 최대 풀 크기를 4로, 작업 대기열 크기를 2로 유지합니다. 따라서 이미 4개의 실행 작업이 있고 현재 더 많은 작업이 할당된 경우 작업 대기열은 그 중 2개(새 작업)만 보유하고 나머지는 RejectedExecutionHandlerImpl에 의해 처리됩니다.

위 프로그램의 출력으로 위의 사항을 확인할 수 있다.

pool-1-thread-1 Start. Command = cmd0
pool-1-thread-4 Start. Command = cmd5
cmd6 is rejected
pool-1-thread-3 Start. Command = cmd4
pool-1-thread-2 Start. Command = cmd1
cmd7 is rejected
cmd8 is rejected
cmd9 is rejected
[monitor] [0/2] Active: 4, Completed: 0, Task: 6, isShutdown: false, isTerminated: false
[monitor] [4/2] Active: 4, Completed: 0, Task: 6, isShutdown: false, isTerminated: false
pool-1-thread-4 End.
pool-1-thread-1 End.
pool-1-thread-2 End.
pool-1-thread-3 End.
pool-1-thread-1 Start. Command = cmd3
pool-1-thread-4 Start. Command = cmd2
[monitor] [4/2] Active: 2, Completed: 4, Task: 6, isShutdown: false, isTerminated: false
[monitor] [4/2] Active: 2, Completed: 4, Task: 6, isShutdown: false, isTerminated: false
pool-1-thread-1 End.
pool-1-thread-4 End.
[monitor] [4/2] Active: 0, Completed: 6, Task: 6, isShutdown: false, isTerminated: false
[monitor] [2/2] Active: 0, Completed: 6, Task: 6, isShutdown: false, isTerminated: false
[monitor] [2/2] Active: 0, Completed: 6, Task: 6, isShutdown: false, isTerminated: false
[monitor] [2/2] Active: 0, Completed: 6, Task: 6, isShutdown: false, isTerminated: false
[monitor] [2/2] Active: 0, Completed: 6, Task: 6, isShutdown: false, isTerminated: false
[monitor] [2/2] Active: 0, Completed: 6, Task: 6, isShutdown: false, isTerminated: false
[monitor] [0/2] Active: 0, Completed: 6, Task: 6, isShutdown: true, isTerminated: true
[monitor] [0/2] Active: 0, Completed: 6, Task: 6, isShutdown: true, isTerminated: true

실행기의 활성 작업, 완료된 작업 및 완료된 모든 작업 수의 변경 사항에 유의하십시오. shutdown() 메서드를 호출하여 제출된 모든 작업을 종료하고 스레드 풀을 종료할 수 있습니다.

 ThreadPoolExecutor의 자세한 사용

사실 자바 쓰레드 풀의 구현 원리는 아주 간단하다, 직설적으로 말하자면 쓰레드 콜렉션 workerSet과 블로킹 큐 workQueue이다. 사용자가 작업(즉, 스레드)을 스레드 풀에 제출하면 스레드 풀은 먼저 작업을 workQueue에 넣습니다. workerSet의 스레드는 계속해서 workQueue에서 스레드를 가져와 실행합니다. workQueue에 작업이 없으면 작업자는 대기열에 작업이 있을 때까지 차단한 다음 꺼내서 실행을 계속합니다.

 실행 원칙

작업이 스레드 풀에 제출된 후:

  1. 먼저 스레드 풀에서 현재 실행 중인 스레드 수가 corePoolSize보다 작은지 여부입니다. 그렇다면 작업을 수행할 새 작업자 스레드를 만듭니다. 모든 작업이 실행 중인 경우 2로 이동합니다.
  2. BlockingQueue가 꽉 찼는지 확인하고 그렇지 않으면 스레드를 BlockingQueue에 넣습니다. 그렇지 않으면 3으로 이동합니다.
  3. 새 작업자 스레드를 생성하면 현재 실행 중인 스레드 수가 maximumPoolSize를 초과하는 경우 작업을 처리하기 위해 RejectedExecutionHandler로 넘겨집니다.

ThreadPoolExecutor가 새 스레드를 생성하면 CAS를 통해 스레드 풀의 상태 ctl을 업데이트합니다.

#매개변수 _

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler)
  • corePoolSize스레드 풀의 코어 스레드 수 태스크가 제출되면 스레드 풀은 현재 스레드 수가 corePoolSize와 같아질 때까지 태스크를 실행할 새 스레드를 생성합니다. 새로운 태스크를 실행할 수 있는 다른 유휴 스레드가 있더라도 스레드 풀은 , 스레드는 계속 생성되며, 현재 스레드 수가 corePoolSize이고 계속 제출되는 작업은 차단 대기열에 저장되고 실행될 때까지 대기합니다. 스레드 풀의 prestartAllCoreThreads() 메서드가 실행되면 , 스레드 풀은 모든 코어 스레드를 미리 생성하고 시작합니다.

  • workQueue실행 대기 중인 작업을 보관하는 데 사용되는 차단 대기열입니다.

    • ArrayBlockingQueue: FIFO로 작업을 정렬하는 배열 구조를 기반으로 하는 제한된 차단 대기열입니다.
    • LinkedBlockingQueue: 연결된 목록 구조를 기반으로 대기열 차단, FIFO로 작업 정렬, 처리량은 일반적으로 ArrayBlockingQueue보다 높습니다.
    • SynchronousQueue: 요소를 저장하지 않는 차단 대기열, 각 삽입 작업은 다른 스레드가 제거 작업을 호출할 때까지 기다려야 합니다. 그렇지 않으면 삽입 작업이 항상 차단되고 처리량이 일반적으로 LinkedBlockingQueue보다 높습니다.
    • PriorityBlockingQueue: 우선 순위가 있는 제한 없는 차단 대기열;

LinkedBlockingQueueArrayBlockingQueue노드를 삽입하고 삭제하는 성능보다는 낫지만 작업을 수행할 때 둘 다 put()잠금 take()이 필요합니다 잠금 SynchronousQueue해제 알고리즘을 사용하여 잠금을 사용하지 않고 노드의 상태에 따라 실행을 판단한다는 것이 핵심입니다 Transfer.transfer().

  • maximumPoolSize 스레드 풀에서 허용되는 최대 스레드 수입니다. 현재 블로킹 큐가 꽉 찼고 작업이 계속 제출되면 현재 스레드 수가 maximumPoolSize보다 작은 경우 작업을 실행하기 위해 새 스레드가 생성됩니다. 코어 스레드 풀에 제출할 수 없기 때문에 작동하지 않습니다. 스레드는 계속 workQueue에 배치됩니다.

  • keepAliveTime 스레드가 유휴 상태일 때의 생존 시간, 즉 스레드에 실행할 작업이 없을 때 스레드는 계속 생존합니다. 시간이 종료됩니다.

  • unit keepAliveTime 단위

  • threadFactory 스레드에 대한 팩토리 생성 사용자 지정 스레드 팩토리를 통해 새로 생성된 각 스레드에 대해 식별 가능한 스레드 이름을 설정할 수 있습니다. 기본값은 DefaultThreadFactory입니다.

  • handler 스레드 풀의 포화 전략.블로킹 대기열이 가득 차고 유휴 작업자 스레드가 없을 때 작업을 계속 제출하는 경우 작업을 처리하는 전략을 채택해야 합니다.스레드 풀은 4가지 전략을 제공합니다.

    • AbortPolicy: 기본 전략인 예외를 직접 발생시킵니다.
    • CallerRunsPolicy: 호출자가 위치한 스레드를 사용하여 작업을 실행합니다.
    • DiscardOldestPolicy: 차단 대기열의 최상위 작업을 버리고 현재 작업을 실행합니다.
    • DiscardPolicy: 작업을 직접 폐기합니다.

물론 애플리케이션 시나리오에 따라 RejectedExecutionHandler 인터페이스를 구현하고 처리할 수 없는 작업을 로깅하거나 지속적으로 저장하는 등 포화 전략을 사용자 지정할 수도 있습니다.

세 가지 유형

newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>());
}

스레드 풀의 스레드 수가 corePoolSize에 도달하면 스레드 풀에 실행 가능한 작업이 없어도 스레드가 해제되지 않습니다.

FixedThreadPool의 작업 대기열은 제한되지 않은 대기열 LinkedBlockingQueue(대기열 용량은 Integer.MAX_VALUE임)이며, 이로 인해 다음과 같은 문제가 발생합니다.

  • 스레드 풀의 스레드 수는 corePoolSize를 초과하지 않으므로 maximumPoolSize 및 keepAliveTime은 쓸모 없는 매개변수가 됩니다.
  • 무제한 대기열의 사용으로 인해 FixedThreadPool은 절대 거부하지 않습니다. 즉, 포화 전략이 실패합니다.

 newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

초기화된 스레드 풀에는 하나의 스레드만 있으며 스레드가 비정상적으로 종료되면 새로운 스레드가 다시 생성되어 작업을 계속 실행합니다.

무제한 대기열의 사용으로 인해 SingleThreadPool은 절대 거부하지 않습니다. 즉, 포화 전략이 실패합니다.

 newCachedThreadPool

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

스레드 풀의 스레드 수는 2147483647인 Integer.MAX_VALUE에 도달할 수 있으며 SynchronousQueue는 내부적으로 차단 대기열로 사용됩니다. 스레드의 유휴 시간이 keepAliveTime을 초과합니다.새 작업을 제출할 때 유휴 스레드가 없으면 작업을 실행하기 위해 새 스레드를 생성하면 특정 시스템 오버헤드가 발생합니다.실행 프로세스는 이전 두 가지와 약간 다릅니다.

  • 메인 쓰레드는 SynchronousQueue의 offer() 메소드를 호출하여 태스크에 집어넣는데, 이때 쓰레드 풀에 SynchronousQueue의 태스크를 읽으려고 시도하는 유휴 쓰레드가 있다면, 즉 SynchronousQueue의 poll() 가 호출되면 메인 스레드는 작업을 유휴 스레드로 넘깁니다. 그렇지 않으면 (2)를 수행합니다.
  • 스레드 풀이 비어 있거나 유휴 스레드가 없는 경우 새 스레드를 생성하여 작업을 실행합니다.
  • 작업을 완료한 스레드가 60초 이내에 여전히 유휴 상태이면 종료되므로 오랫동안 유휴 상태인 CachedThreadPool은 스레드 리소스를 보유하지 않습니다.

스레드 풀을 닫습니다.

스레드 풀의 모든 스레드를 순회하고 스레드의 인터럽트 메서드를 하나씩 호출하여 스레드를 인터럽트합니다.

종료 방법 - 종료

스레드 풀의 스레드 상태를 SHUTDOWN 상태로 설정한 다음 작업을 실행하지 않는 모든 스레드를 중단합니다.

종료 방법 - shutdownNow

스레드 풀의 스레드 상태를 STOP 상태로 설정한 다음 작업을 실행 중이거나 일시 중지하는 모든 스레드를 중지합니다.이 두 가지 종료 방법 중 하나를 호출하면 isShutDown()이 true를 반환합니다.모든 작업이 성공적으로 종료되면 isTerminated()가 반환합니다. 진실.

ThreadPoolExecutor의 상세 소스 코드

몇 가지 핵심 속성

//这个属性是用来存放 当前运行的worker数量以及线程池状态的
//int是32位的,这里把int的高3位拿来充当线程池状态的标志位,后29位拿来充当当前运行worker的数量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//存放任务的阻塞队列
private final BlockingQueue<Runnable> workQueue;
//worker的集合,用set来存放
private final HashSet<Worker> workers = new HashSet<Worker>();
//历史达到的worker数最大值
private int largestPoolSize;
//当队列满了并且worker的数量达到maxSize的时候,执行具体的拒绝策略
private volatile RejectedExecutionHandler handler;
//超出coreSize的worker的生存时间
private volatile long keepAliveTime;
//常驻worker的数量
private volatile int corePoolSize;
//最大worker的数量,一般当workQueue满了才会用到这个参数
private volatile int maximumPoolSize;

내부 상태

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

// Packing and unpacking ctl
private static int runStateOf(int c)     { return c & ~CAPACITY; }
private static int workerCountOf(int c)  { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }

그중에서 AtomicInteger 변수 ctl은 매우 강력합니다. 하위 29비트를 사용하여 스레드 풀의 스레드 수를 나타내고 상위 3비트를 사용하여 스레드 풀의 실행 상태를 나타냅니다.

  • RUNNING: -1 << COUNT_BITS, 즉 상위 3비트는 111입니다. 이 상태의 스레드 풀은 차단 대기열에서 새 작업을 수신하고 작업을 처리합니다.
  • SHUTDOWN: 0 << COUNT_BITS, 즉 상위 3비트가 000이면 이 상태의 스레드 풀은 새 작업을 받지 않지만 차단 대기열의 작업을 처리합니다.
  • STOP : 1 << COUNT_BITS, 즉 상위 3비트는 001이며 이 상태의 스레드는 새 작업을 받지도 않고 차단 대기열의 작업을 처리하지도 않으며 실행 중인 작업을 중단합니다.
  • TIDYING : 2 << COUNT_BITS, 즉 상위 3비트가 010이고 모든 작업이 종료되었습니다.
  • TERMINATED: 3 << COUNT_BITS, 즉 상위 3비트가 011이면 종료() 메서드가 실행되었습니다.

# 작업 실행

실행 –> addWorker –>runworker(getTask)

스레드 풀의 작업 스레드는 Woker 클래스에 의해 구현되며 ReentrantLock 잠금을 보장하여 Woker 인스턴스가 HashSet에 삽입되고 Woker의 스레드가 시작됩니다. Woker 클래스의 생성 메소드 구현에서 스레드 팩토리가 스레드 스레드를 생성할 때 Woker 인스턴스 자체에 이것을 매개변수로 전달하고 시작 메소드를 실행하여 스레드 스레드를 시작한다는 것을 알 수 있습니다. 본질은 작업자의 runWorker 메서드를 실행하는 것입니다. firstTask의 실행이 완료된 후 getTask 메소드를 통해 차단 대기열에서 대기 작업을 가져오고 대기열에 작업이 없으면 getTask 메소드가 차단되고 일시 중지되며 CPU 리소스를 점유하지 않습니다.

# 실행() 메소드

ThreadPoolExecutor.execute(태스크)Executor.execute(태스크)

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    /*
     * Proceed in 3 steps:
     *
     * 1. If fewer than corePoolSize threads are running, try to
     * start a new thread with the given command as its first
     * task.  The call to addWorker atomically checks runState and
     * workerCount, and so prevents false alarms that would add
     * threads when it shouldn't, by returning false.
     *
     * 2. If a task can be successfully queued, then we still need
     * to double-check whether we should have added a thread
     * (because existing ones died since last checking) or that
     * the pool shut down since entry into this method. So we
     * recheck state and if necessary roll back the enqueuing if
     * stopped, or start a new thread if there are none.
     *
     * 3. If we cannot queue task, then we try to add a new
     * thread.  If it fails, we know we are shut down or saturated
     * and so reject the task.
     */
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {  
    //workerCountOf获取线程池的当前线程数;小于corePoolSize,执行addWorker创建新线程执行command任务
       if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // double check: c, recheck
    // 线程池处于RUNNING状态,把提交的任务成功放入阻塞队列中
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // recheck and if necessary 回滚到入队操作前,即倘若线程池shutdown状态,就remove(command)
        //如果线程池没有RUNNING,成功从阻塞队列中删除任务,执行reject方法处理任务
        if (! isRunning(recheck) && remove(command))
            reject(command);
        //线程池处于running状态,但是没有线程,则创建线程
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 往线程池中创建新的线程失败,则reject任务
    else if (!addWorker(command, false))
        reject(command);
}
  • 스레드 풀의 상태를 다시 확인해야 하는 이유는 무엇입니까?

멀티 쓰레드 환경에서 쓰레드 풀의 상태는 시시각각 변하고 있으며, ctl.get()은 비원자적 연산이다. 스레드 풀을 얻습니다. workque에 명령을 추가할지 여부를 판단하는 것은 스레드 풀 이전 상태입니다. 이중 확인이 없으면 스레드 풀이 실행되지 않는 상태(멀티 스레드 환경에서 발생할 수 있음)에 있으면 명령이 실행되지 않습니다.

# addWorker 메소드

execute 메서드의 구현에서 우리는 addWorker가 주로 새 스레드를 생성하고 작업을 실행하는 것을 담당한다는 것을 알 수 있습니다. 스레드 풀이 작업을 수행하기 위해 새 스레드를 생성할 때 전역 잠금을 획득해야 합니다.

private final ReentrantLock mainLock = new ReentrantLock();
private boolean addWorker(Runnable firstTask, boolean core) {
    // CAS更新线程池数量
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
                firstTask == null &&
                ! workQueue.isEmpty()))
            return false;

        for (;;) {
            int wc = workerCountOf(c);
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            // 线程池重入锁
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                // Recheck while holding lock.
                // Back out on ThreadFactory failure or if
                // shut down before lock acquired.
                int rs = runStateOf(ctl.get());

                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                t.start();  // 线程启动,执行任务(Worker.thread(firstTask).start());
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

# Worker 클래스의 runworker 메소드

 private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
     Worker(Runnable firstTask) {
         setState(-1); // inhibit interrupts until runWorker
         this.firstTask = firstTask;
         this.thread = getThreadFactory().newThread(this); // 创建线程
     }
     /** Delegates main run loop to outer runWorker  */
     public void run() {
         runWorker(this);
     }
     // ...
 }
  • AQS 클래스를 계승하여 작업자 스레드의 중단 작업을 구현하는 것이 편리합니다.
  • 작업자 스레드에서 작업으로 실행될 수 있는 Runnable 인터페이스를 구현합니다.
  • 현재 제출된 태스크 firstTask는 매개변수로 Worker 생성자에 전달됩니다.

일부 속성에는 생성자도 있습니다.

//运行的线程,前面addWorker方法中就是直接通过启动这个线程来启动这个worker
final Thread thread;
//当一个worker刚创建的时候,就先尝试执行这个任务
Runnable firstTask;
//记录完成任务的数量
volatile long completedTasks;

Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    //创建一个Thread,将自己设置给他,后面这个thread启动的时候,也就是执行worker的run方法
    this.thread = getThreadFactory().newThread(this);
}   

runWorker 메서드는 스레드 풀의 핵심입니다.

  • 스레드가 시작된 후 잠금 해제 방법을 통해 잠금이 해제되고 AQS의 상태가 0으로 설정되어 작업을 중단할 수 있음을 나타냅니다.
  • Worker는 firstTask를 실행하거나 workQueue에서 작업을 가져옵니다.
    • 잠금 작업을 수행하여 스레드가 다른 스레드에 의해 중단되지 않도록 합니다(스레드 풀이 중단되지 않는 한).
    • 스레드 풀 상태를 확인하십시오. 스레드 풀이 중단되면 현재 스레드가 중단됩니다.
    • 실행 전에실행
    • 작업을 실행하는 run 메서드
    • afterExecute 메소드 실행
    • 잠금 해제 작업

getTask 메서드를 통해 차단 대기열에서 대기 작업을 가져옵니다.대기열에 작업이 없으면 getTask 메서드는 차단되고 일시 중지되며 CPU 리소스를 차지하지 않습니다.

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        // 先执行firstTask,再从workerQueue中取task(getTask())

        while (task != null || (task = getTask()) != null) {
            w.lock();
            // If pool is stopping, ensure thread is interrupted;
            // if not, ensure thread is not interrupted.  This
            // requires a recheck in second case to deal with
            // shutdownNow race while clearing interrupt
            if ((runStateAtLeast(ctl.get(), STOP) ||
                    (Thread.interrupted() &&
                    runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

# getTask 메소드

keepAliveTime을 사용하는 getTask() 메서드를 살펴보자 이 메서드에서 스레드 풀이 corePoolSize를 초과하는 작업자 부분을 어떻게 파괴하는지 확인할 수 있습니다.

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);

        // Are workers subject to culling?
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

이 코드 조각은 keepAliveTime 작동의 핵심입니다.

boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();

allowCoreThreadTimeOut이 false이면 스레드는 유휴 상태이더라도 파기되지 않으며, true이면 keepAliveTime 내에서 유휴 상태이면 파기됩니다.

스레드가 소멸되지 않고 유휴 상태로 대기할 수 있는 경우 timed == false, workQueue.take 작업: 차단 대기열이 비어 있으면 현재 스레드가 일시 중지되고 대기하며 작업이 대기열에 추가되면 스레드는 깨우고 take 메서드는 작업을 반환하고 실행합니다.

스레드가 무한 유휴 시간 == true를 허용하지 않는 경우 workQueue.poll 작업: 차단 대기열에 여전히 keepAliveTime 시간 내에 작업이 없으면 null을 반환합니다.

 작업 제출

  1. 작업을 제출하고 스레드 풀이 실행될 때까지 기다립니다.
  2. FutureTask 클래스의 get 메서드가 실행되면 메인 스레드는 WaitNode 노드로 캡슐화되어 waiters 목록에 저장되고 실행 결과를 기다리도록 차단됩니다.
  3. FutureTask 작업 수행 완료 후 UNSAFE를 통해 waiter에 해당하는 waitNode를 null로 설정하고 LockSupport 클래스의 unpark 메서드를 통해 메인 스레드를 깨웁니다.
public class Test{
    public static void main(String[] args) {

        ExecutorService es = Executors.newCachedThreadPool();
        Future<String> future = es.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return "future result";
            }
        });
        try {
            String result = future.get();
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

실제 비즈니스 시나리오에서 Future와 Callable은 기본적으로 쌍으로 나타나며 Callable은 결과 생성을 담당하고 Future는 결과 획득을 담당합니다.

  1. Callable 인터페이스는 Runnable이 값을 반환하지 않는다는 점을 제외하면 Runnable과 유사합니다.
  2. Callable 작업의 정상적인 결과를 반환하는 것 외에도 예외가 발생하면 예외도 반환됩니다. 즉, Future는 비동기 실행 작업의 다양한 결과를 얻을 수 있습니다.
  3. Future.get 메서드는 Callable 작업이 실행될 때까지 메인 스레드를 차단합니다.

# 제출 방법

AbstractExecutorService.submit()은 ExecutorService.submit()을 구현하여 실행 후 반환 값을 얻고 ThreadPoolExecutor는 AbstractExecutorService.submit()의 하위 클래스이므로 submit 메서드도 ThreadPoolExecutor`의 메서드입니다.

// submit()在ExecutorService中的定义
<T> Future<T> submit(Callable<T> task);

<T> Future<T> submit(Runnable task, T result);

Future<?> submit(Runnable task);
// submit方法在AbstractExecutorService中的实现
public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();
    // 通过submit方法提交的Callable任务会被封装成了一个FutureTask对象。
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}

제출 방법을 통해 제출된 호출 가능한 작업은 FutureTask 객체로 캡슐화됩니다. Executor.execute 메서드를 통해 FutureTask를 스레드 풀에 제출하여 실행을 대기하고 최종 실행은 FutureTask의 run 메서드입니다.

FutureTask 개체

public class FutureTask<V> implements RunnableFuture<V>FutureTask는 실행될 스레드 풀에 제출될 수 있습니다(FutureTask의 run 메소드에 의해 실행됨).

  • 내부 상태
/* The run state of this task, initially NEW. 
    * ...
    * Possible state transitions:
    * NEW -> COMPLETING -> NORMAL
    * NEW -> COMPLETING -> EXCEPTIONAL
    * NEW -> CANCELLED
    * NEW -> INTERRUPTING -> INTERRUPTED
    */
private volatile int state;
private static final int NEW          = 0;
private static final int COMPLETING   = 1;
private static final int NORMAL       = 2;
private static final int EXCEPTIONAL  = 3;
private static final int CANCELLED    = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED  = 6;

내부 상태의 수정은 sun.misc.Unsafe에 의해 수정됩니다.

  • 방법을 얻을
public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    return report(s);
} 

메인 스레드는 awaitDone 메서드를 통해 내부적으로 차단되며 구체적인 구현은 다음과 같습니다.

private int awaitDone(boolean timed, long nanos)
    throws InterruptedException {
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    WaitNode q = null;
    boolean queued = false;
    for (;;) {
        if (Thread.interrupted()) {
            removeWaiter(q);
            throw new InterruptedException();
        }

        int s = state;
        if (s > COMPLETING) {
            if (q != null)
                q.thread = null;
            return s;
        }
        else if (s == COMPLETING) // cannot time out yet
            Thread.yield();
        else if (q == null)
            q = new WaitNode();
        else if (!queued)
            queued = UNSAFE.compareAndSwapObject(this, waitersOffset,q.next = waiters, q);
        else if (timed) {
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) {
                removeWaiter(q);
                return state;
            }
            LockSupport.parkNanos(this, nanos);
        }
        else
            LockSupport.park(this);
    }
}
  1. 메인 스레드가 중단되면 인터럽트 예외가 발생합니다.
  2. FutureTask의 현재 상태를 결정합니다. COMPLETING보다 크면 작업이 실행되었음을 의미하고 직접 반환됩니다.
  3. 현재 상태가 COMPLETING이면 작업이 실행되었음을 의미하며 이때 메인 스레드는 yield 메서드를 통해 cpu 리소스를 양보하고 상태가 NORMAL이 될 때까지 기다리면 됩니다.
  4. WaitNode 클래스를 통해 현재 스레드를 캡슐화하고 UNSAFE를 통해 대기자 목록에 추가합니다.
  5. 마지막으로 LockSupport의 park 또는 parkNanos를 통해 스레드를 일시 중단합니다.

실행 방법

public void run() {
    if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))
        return;
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex);
            }
            if (ran)
                set(result);
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        runner = null;
        // state must be re-read after nulling runner to prevent
        // leaked interrupts
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

FutureTask.run 메서드는 메인 스레드가 아닌 스레드 풀에서 실행됩니다.

  1. Callable 태스크의 호출 메소드를 실행하여;
  2. 호출이 성공적으로 수행되면 결과는 set 메소드를 통해 저장됩니다.
  3. 호출 실행에 예외가 있으면 setException을 통해 예외를 저장합니다.

작업 종료

종료 메소드는 스레드 풀의 상태를 SHUTDOWN으로 설정하고 스레드 풀이 이 상태에 들어간 후 작업 수락을 거부하고 나머지 모든 작업을 실행합니다.

public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        //检查是否可以关闭线程
        checkShutdownAccess();
        //设置线程池状态
        advanceRunState(SHUTDOWN);
        //尝试中断worker
        interruptIdleWorkers();
            //预留方法,留给子类实现
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
}

private void interruptIdleWorkers() {
    interruptIdleWorkers(false);
}

private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        //遍历所有的worker
        for (Worker w : workers) {
            Thread t = w.thread;
            //先尝试调用w.tryLock(),如果获取到锁,就说明worker是空闲的,就可以直接中断它
            //注意的是,worker自己本身实现了AQS同步框架,然后实现的类似锁的功能
            //它实现的锁是不可重入的,所以如果worker在执行任务的时候,会先进行加锁,这里tryLock()就会返回false
            if (!t.isInterrupted() && w.tryLock()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                } finally {
                    w.unlock();
                }
            }
            if (onlyOne)
                break;
        }
    } finally {
        mainLock.unlock();
    }
}

ShutdownNow는 비교적 잘 작동하며 먼저 스레드 풀 상태를 STOP으로 설정한 다음 제출된 모든 작업을 거부합니다. 마지막으로 실행 중인 왼쪽 및 오른쪽 작업자를 중단한 다음 작업 대기열을 지웁니다.

public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        //检测权限
        advanceRunState(STOP);
        //中断所有的worker
        interruptWorkers();
        //清空任务队列
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
    return tasks;
}

private void interruptWorkers() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        //遍历所有worker,然后调用中断方法
        for (Worker w : workers)
            w.interruptIfStarted();
    } finally {
        mainLock.unlock();
    }
}

 더 깊은 이해

스레드 풀이 Executor를 사용하여 생성할 수 없는 이유는 무엇입니까?권장하는 방법은 무엇입니까?

스레드 풀은 Executor를 사용하여 생성할 수 없으며 ThreadPoolExecutor를 통해 생성할 수 있습니다. 설명: Executor의 각 방법의 단점:

  • newFixedThreadPool 및 newSingleThreadExecutor: 주요 문제는 누적된 요청 처리 대기열이 매우 큰 메모리 또는 OOM을 소비할 수 있다는 것입니다.
  • newCachedThreadPool 및 newScheduledThreadPool: 주된 문제는 최대 스레드 수가 Integer.MAX_VALUE라는 것입니다. 이로 인해 매우 많은 스레드 또는 OOM이 생성될 수 있습니다.

 추천 방법 1

최초 도입: commons-lang3 패키지

ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,
        new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());

#권장 방법 2

최초 도입: com.google.guava 패키지

ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();

//Common Thread Pool
ExecutorService pool = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

// excute
pool.execute(()-> System.out.println(Thread.currentThread().getName()));

 //gracefully shutdown
pool.shutdown();

#권장 방법 3

Spring 구성 스레드 풀 방식: 커스텀 스레드 팩토리 bean은 ThreadFactory를 구현해야 하며, 이 인터페이스의 다른 기본 구현 클래스를 참조하고 이를 사용하여 bean을 직접 주입하고 execute(Runnable task) 메서드를 호출할 수 있습니다.

    <bean id="userThreadPool" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
        <property name="corePoolSize" value="10" />
        <property name="maxPoolSize" value="100" />
        <property name="queueCapacity" value="2000" />

    <property name="threadFactory" value= threadFactory />
        <property name="rejectedExecutionHandler">
            <ref local="rejectedExecutionHandler" />
        </property>
    </bean>
    
    //in code
    userThreadPool.execute(thread);

 스레드 풀 구성 시 고려해야 할 요소

작업 우선 순위, 작업 실행 시간, 작업 특성(CPU 집약적/IO 집약적) 및 작업 종속성의 네 가지 관점에서. 제한된 작업 대기열을 최대한 가깝게 사용하십시오.

성격이 다른 작업은 크기가 다른 스레드 풀을 사용하여 개별적으로 처리할 수 있습니다.

  • CPU 집약적: 가능한 적은 수의 스레드, Ncpu+1
  • IO 집약적: 가능한 한 많은 스레드, 데이터베이스 연결 풀과 같은 Ncpu*2
  • 하이브리드: CPU 집약적인 작업과 IO 집약적인 작업은 실행 시간의 차이가 거의 없고 두 개의 스레드 풀로 분할되며 그렇지 않으면 분할할 필요가 없습니다.

 스레드 풀의 상태 모니터링

ThreadPoolExecutor의 다음 메서드를 사용할 수 있습니다.

  • getTaskCount()실행이 예약된 작업의 대략적인 총 수를 반환합니다.
  • getCompletedTaskCount()실행이 완료된 작업의 대략적인 총 수를 반환합니다. getTaskCount()보다 적은 결과를 반환합니다.
  • getLargestPoolSize()풀에 동시에 있었던 최대 스레드 수를 반환합니다. 반환된 결과는 maximumPoolSize보다 작거나 같습니다.
  • getPoolSize()풀의 현재 스레드 수를 반환합니다.
  • getActiveCount()능동적으로 작업을 실행 중인 대략적인 스레드 수를 반환합니다.

 참조 기사

  • Java 동시 프로그래밍 기술
  • https://www.jianshu.com/p/87bff5cc8d8c
  • https://blog.csdn.net/programmer_at/article/details/79799267
  • https://blog.csdn.net/u013332124/article/details/79587436
  • https://www.journaldev.com/1069/threadpoolexecutor-java-thread-pool-example-executorservice

추천

출처blog.csdn.net/a619602087/article/details/130527276