記事を読んでJavaスレッドプールを理解する --ThreadPoolExecutorの詳細説明

まずは理解するための質問から

  • なぜスレッドプールがあるのでしょうか?
  • Java がスレッド プールを実装および管理する方法は何ですか?その使用方法の簡単な例を教えてください。
  • 多くの企業がスレッド プールの作成に Executor を使用することを許可していないのはなぜですか? では、Executor をどのように使用することをお勧めしますか?
  • ThreadPoolExecutor のコア構成パラメータは何ですか?簡単に説明してください
  • ThreadPoolExecutor が作成できる 3 つのスレッド プールはどれですか?
  • キューがいっぱいでワーカーの数が maxSize に達するとどうなりますか?
  • ThreadPoolExecutor にはどのような RejectedExecutionHandler 戦略があるのか​​教えてください? デフォルトの戦略は何ですか?
  • スレッドプールのタスク実行メカニズムについて簡単に説明します?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;
    }
}
  • シンプルスレッドプール
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 つのスレッドが完了しても停止しないことを示しています。 work は常に存在し、スレッド プールが終了するまでスレッド プールに割り当てられたタスクを実行します。

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 を使用したスレッド プールの実装例を次に示します。

  • ワーカープール.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の詳しい使い方

実は、Java スレッド プールの実装原理は非常に単純で、端的に言えば、スレッド コレクションの workSet とブロッキング キュー 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()SynchronousQueueTransfer.transfer()

  • maximumPoolSize スレッド プールで許可されるスレッドの最大数。現在のブロッキング キューがいっぱいで、タスクが引き続き送信される場合、現在のスレッド数が maxPoolSize 未満であれば、タスクを実行するために新しいスレッドが作成されます。ブロッキング キューが無制限のキューの場合、maximumPoolSizeコア スレッド プールに送信できないため、機能しません。スレッドは継続的に workQueue に入れられます。

  • keepAliveTime スレッドがアイドル状態のとき、つまりスレッドに実行するタスクがないとき、スレッドは存続し続ける時間。デフォルトでは、このパラメータは、スレッドの数が corePoolSize よりも大きく、アイドル状態のスレッドがこれを超える場合にのみ役立ちます。時間は終了します。

  • unit keepAliveTimeの単位

  • threadFactory スレッドのファクトリを作成する: カスタム スレッド ファクトリを通じて、新しく作成された各スレッドに識別可能なスレッド名を設定できます。デフォルトは DefaultThreadFactory です

  • handler スレッド プールの飽和戦略。ブロッキング キューがいっぱいで、アイドル状態のワーカー スレッドがない場合、タスクの送信を続ける場合は、タスクを処理する戦略を採用する必要があります。スレッド プールには、次の 4 つの戦略が用意されています。

    • AbortPolicy: 例外を直接スローします。デフォルトの戦略です。
    • CallerRunsPolicy: 呼び出し元が存在するスレッドを使用してタスクを実行します。
    • DiscardOldestPolicy: ブロッキング キューの先頭のタスクを破棄し、現在のタスクを実行します。
    • DiscardPolicy: タスクを直接破棄します。

もちろん、アプリケーションのシナリオに応じて RejectedExecutionHandler インターフェイスを実装し、処理できないタスクのログ記録や永続的な保存などの飽和戦略をカスタマイズすることもできます。

3種類

新しい固定スレッドプール

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>()));
}

初期化されたスレッド プールにはスレッドが 1 つだけあります。スレッドが異常終了した場合は、新しいスレッドが再作成されてタスクの実行を継続します。投入されたタスクの順序実行を保証できるのは、そのスレッドのみです。

無制限のキューを使用するため、SingleThreadPool は決して拒否しません。つまり、飽和戦略は失敗します。

 新しいキャッシュされたスレッドプール

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

スレッド プール内のスレッドの数は Integer.MAX_VALUE (2147483647) に達する可能性があり、SynchronousQueue はブロッキング キューとして内部的に使用されます。newFixedThreadPool によって作成されたスレッド プールとは異なり、newCachedThreadPool は実行するタスクがない場合にスレッド リソースを自動的に解放します。スレッドのアイドル時間が keepAliveTime を超えています。新しいタスクを送信するときに、アイドル状態のスレッドがない場合、タスクを実行するための新しいスレッドを作成すると、一定のシステム オーバーヘッドが発生します。実行プロセスは、前の 2 つとは若干異なります。

  • メインスレッドは SynchronousQueue の offer() メソッドを呼び出してタスクに入れますが、この時点で SynchronousQueue のタスクを読み取ろうとしているアイドル状態のスレッドがスレッド プールに存在する場合、つまり SynchronousQueue の poll() メソッドが呼び出されます。が呼び出されると、メインスレッドはタスクをアイドルスレッドに渡します。それ以外の場合は (2) を実行します。
  • スレッド プールが空であるか、アイドル状態のスレッドがない場合は、新しいスレッドを作成してタスクを実行します。
  • タスクの実行を終了したスレッドが 60 秒以内にアイドル状態のままである場合、タスクは終了されるため、長時間アイドル状態が続いた CachedThreadPool にはスレッド リソースが保持されません。

スレッドプールを閉じる

スレッド プール内のすべてのスレッドを走査し、スレッドの割り込みメソッドを 1 つずつ呼び出してスレッドを中断します。

シャットダウン方法 - シャットダウン

スレッド プールのスレッド状態を SHUTDOWN 状態に設定し、タスクを実行していないすべてのスレッドを中断します。

シャットダウン方法 - shutdownNow

スレッド プールのスレッド状態を STOP 状態に設定し、タスクを実行または一時停止しているすべてのスレッドを停止します。これら 2 つのシャットダウン メソッドのいずれかを呼び出すだけで、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、terminated() メソッドが実行されました

#タスクの実行

実行 –> addWorker –> runworker (getTask)

スレッド プールの作業スレッドは Woker クラスによって実装され、ReentrantLock ロックの保証の下、HashSet に Woker インスタンスが挿入され、Woker 内のスレッドが開始されます。Wker クラスの構築メソッドの実装から、スレッド ファクトリがスレッド スレッドを作成するときに、これをパラメータとして Wker インスタンス自体に渡していることがわかります。スレッド スレッドを開始するために start メソッドが実行されると、本質は、ワーカーの runWorker メソッドを実行することです。firstTask の実行が完了すると、getTask メソッドを通じて待機中のタスクがブロッキング キューから取得されます。キューにタスクがない場合、getTask メソッドはブロックされて中断され、CPU リソースを占有しません。

#execute ()メソッド

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() は非アトミックな操作であるため、スレッド プールのステータスがステータスの直後に変更される可能性が非常に高くなります。スレッドプールが取得されます。ワークキューにコマンドを追加するかどうかの判断はスレッドプール前の状態です。二重チェックが行われず、スレッド プールが非実行状態にある場合 (これはマルチスレッド環境で起こりやすい)、コマンドは決して実行されません。

# addWorker メソッド

実行メソッドの実装から、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 メソッドはスレッド プールの中核です。

  • スレッドが開始されると、unlock メソッドによってロックが解放され、AQS の状態が 0 に設定され、操作を中断できることを示します。
  • ワーカーは 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 task: ブロッキング キューが空の場合、現在のスレッドは一時停止されて待機します。タスクがキューに追加されると、スレッドは目覚めると、take メソッドはタスクを返し、実行します。

スレッドが無限のアイドル時間を許可しない場合、timed == true、workQueue.poll タスク: keepAliveTime 時間内にブロッキング キューにまだタスクがない場合は、null を返します。

 タスクの提出

  1. タスクを送信し、スレッド プールが実行されるのを待ちます
  2. FutureTask クラスの get メソッドが実行されると、メイン スレッドは WaitNode ノードにカプセル化されてウェイター リストに格納され、実行結果を待つためにブロックされます。
  3. FutureTask タスクの実行が完了したら、UNSAFE を通じてウェイターに対応する 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;
}

submit メソッドを通じて送信された Callable タスクは、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 タスクの call メソッドを実行することによって。
  2. 呼び出しが正常に実行された場合、結果は set メソッドを通じて保存されます。
  3. 呼び出しの実行中に例外が発生した場合は、setException を通じて例外を保存します。

タスクの終了

shutdown メソッドは、スレッド プールの状態を 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 集中型)、タスクの依存関係の 4 つの観点から説明します。そして、境界のある作業キューをできるだけ近くで使用します。

異なる性質のタスクは、異なるサイズのスレッド プールを使用して個別に処理できます。

  • CPU 集中型: 可能な限り少ないスレッド、Ncpu+1
  • IO集中型: できるだけ多くのスレッド、Ncpu*2、データベース接続プールなど
  • ハイブリッド: CPU 集中型のタスクと IO 集中型のタスクは実行時間にほとんど差がなく、2 つのスレッド プールに分割されます。それ以外の場合、分割する必要はありません。

 スレッドプールの状態を監視する

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