Anarchy In the 1K

ThreadPoolExecutorの調査

背景

 Javaでスレッドプールを扱う際に、いつもお世話になっているThreadPoolExecutorに関して、ExecutorsクラスのnewXyzThreadPoolメソッドを用いて生成するだけで、その中身を意識したことはありませんでした。今回はそのThreadPoolExecutor関して、調べた結果をまとめます。

調査結果

ThreadPoolExecutorの構成

 下図の通りタスクが格納されるTask Queueと、スレッドが格納されるThread Poolで構成されています。

f:id:fujiU:20200204171514p:plain

Task QueueとThread Poolを用いることで、以下が実現できます。

  • タスク数に対してスレッド数が少ない場合(リクエスト過多)でも、タスクがTask Queueにバッファリングされる為、(Task Queueの上限までは)タスクの依頼が可能になる。
  • Thread Poolのサイズを正しく調整することで、スレッド数が少なすぎる為にプロセッサが遊んでいる状態や、スレッド数が多すぎる為にリソースが不足する状態を防ぐことができる。

ThreadPoolExecutorのタスク依頼時の挙動

 executeメソッドを呼び出し、タスクを依頼した際の挙動に関して、Thread Poolの容量に関連する以下変数とスレッド数の関係によって異なります。

  • corePoolSize: アイドル状態でもプール内に維持されるスレッド数
  • maxPoolSize: プール内の最大スレッド数

具体的には、下図の3パターンに分類されます。 f:id:fujiU:20200207165802p:plain

maxPoolSize = スレッド数の状態でタスクが拒否された場合、デフォルトでは、RejectedExecutionExceptionがスローされます。

Task Queueに使用するキューの種類

 ThreadPoolExecutorのコンストラクタを呼び出す際に、引数としてTask Queueを渡します。Task Queueに使用するキューの種類を以下にまとめます。

 まず、下図の通りキューが要素を保持するものと、しないもので大別されます。次に、キューが要素を保持するものに関して、キューの容量を明示的に設定するものと、しないものに分類されます。 f:id:fujiU:20200207182814p:plain

結果として、キューは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:合わせて、「タスク追加時の挙動」をご覧下さい。