ThreadPoolExecutorの調査
背景
Javaでスレッドプールを扱う際に、いつもお世話になっているThreadPoolExecutor
に関して、Executors
クラスのnewXyzThreadPool
メソッドを用いて生成するだけで、その中身を意識したことはありませんでした。今回はそのThreadPoolExecutor
関して、調べた結果をまとめます。
調査結果
ThreadPoolExecutor
の構成
下図の通りタスクが格納されるTask Queueと、スレッドが格納されるThread Poolで構成されています。
Task QueueとThread Poolを用いることで、以下が実現できます。
- タスク数に対してスレッド数が少ない場合(リクエスト過多)でも、タスクがTask Queueにバッファリングされる為、(Task Queueの上限までは)タスクの依頼が可能になる。
- Thread Poolのサイズを正しく調整することで、スレッド数が少なすぎる為にプロセッサが遊んでいる状態や、スレッド数が多すぎる為にリソースが不足する状態を防ぐことができる。
ThreadPoolExecutor
のタスク依頼時の挙動
execute
メソッドを呼び出し、タスクを依頼した際の挙動に関して、Thread Poolの容量に関連する以下変数とスレッド数の関係によって異なります。
- corePoolSize: アイドル状態でもプール内に維持されるスレッド数
- maxPoolSize: プール内の最大スレッド数
具体的には、下図の3パターンに分類されます。
maxPoolSize = スレッド数
の状態でタスクが拒否された場合、デフォルトでは、RejectedExecutionException
がスローされます。
Task Queueに使用するキューの種類
ThreadPoolExecutor
のコンストラクタを呼び出す際に、引数としてTask Queueを渡します。Task Queueに使用するキューの種類を以下にまとめます。
まず、下図の通りキューが要素を保持するものと、しないもので大別されます。次に、キューが要素を保持するものに関して、キューの容量を明示的に設定するものと、しないものに分類されます。
結果として、キューは3種類に分類され、ぞれぞれ以下の違いがあります。
キューが要素を保持する。
キューの容量を明示的に設定する。
ArrayBlockingQueue
などが該当します。上限を明示的に設定する為、リクエスト過多によるリソース不足を防ぐことができます。その一方で、キューの上限としてどの様な値が妥当か、検証を行う必要があります。
キューの容量を明示的に設定しない。
LinkedBlockingQueue
などが該当します。リソースが許す限りTask Queueにタスクの格納が可能である為、スレッド数はcorePoolSize
を超えません*1。一時的にリクエスト過多な状態を処理するには便利ですが、恒常的にリクエスト過多な状態が続くとキューの容量が増え続けてしまいます。
Executors
クラスのnewFixedThreadPool
, newSingleThreadExecutor
メソッドを用いて、ThreadPoolExecutor
を生成した際のTask Queueはこちらになります。
キューが要素を保持しない。
SynchronousQueue
が該当します。Task Queueへタスクの格納を行わず、直接Thread Poolにタスクを渡します。その為、依頼できるタスクの上限は、maxPoolSize
までになります。
Executors
クラスのnewCachedThreadPool
メソッドを用いて、ThreadPoolExecutor
を生成した際のTask Queueはこちらになります。
ThreadPoolExecutor
の終了
ThreadPoolExecutor
の終了に関して、後述するshutdown
メソッドとshutdownNow
メソッドが用意されています。全てのタスクが処理された後、ThreadPoolExecutor
は、終了スタータスになります。
shutdown
メソッド
shutdown
メソッドは、以下の通り順序正しくタスクを処理して終了します。
- メソッド呼び出し時に実行中だったタスク
タスクが完了するまで待つ。 - メソッド呼び出し時に待機中だったタスク
タスクが完了するまで待つ。 - メソッド呼び出し後に依頼されたタスク
タスクは拒否され、デフォルトではRejectedExecutionException
がスローさる。
shutdownNow
メソッド
shutdownNow
メソッドは、以下の通りタスクを処理して即時終了を試みます。
- メソッド呼び出し時に実行中だったタスク
interrupt
メソッドを呼び出して、タスクのキャンセルを試みる。 - メソッド呼び出し時に待機中だったタスク
タスクを実行せず、戻り値で該当するタスクのリストを返却する。 - メソッド呼び出し後に依頼されたタスク
タスクは拒否され、デフォルトではRejectedExecutionException
がスローされる。
*1:合わせて、「タスク追加時の挙動」をご覧下さい。