実際に使用したときに、私が悩んだ項目であるコンストラクタ引数と、タスク登録について紹介したいと思います。
■コンストラクタ引数についてThreadPoolExecutorには下記の4つのコンストラクタがあります。
1.ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue)
2.ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, RejectedExecutionHandler handler)
3.ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory)
4.ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
コンストラクタ引数のうち、私が特に悩んだRejectedExecutionHandlerについて説明したいと思います。
この値(RejectedExecutionHandler)が何かと言うと、JavaDoc的には下記の様に記述されています。
executor がシャットダウンしている場合、または executor が最大スレッド数とワークキュー容量の両方で有限の境界を使用し、かつ飽和状態である場合、execute(java.lang.Runnable) メソッドで送信された新しいタスクは拒否されます。どちらの場合も、execute メソッドは、RejectedExecutionHandler の RejectedExecutionHandler.rejectedExecution(java.lang.Runnable, java.util.concurrent.ThreadPoolExecutor) メソッドを呼び出します。
要するに、タスク(スレッドで処理されるもの)が処理できなくなった(あふれた)場合に呼び出される処理ということです。
そして、この処理はデフォルトで4種類のポリシーが用意されています。(独自で用意することもできますが)
このポリシーの選択が重要になります!このポリシーの選択を間違えると、処理して欲しいハズの処理が飛ばされたり、無視されたり、例外を投げられたりすることになります!
なので、このポリシーの選択は慎重に行ってください。
ポリシーは下記の4つが存在します。
ThreadPoolExecutor.AbortPolicy RejectedExecutionException をスローする拒否されたタスクのハンドラです。ThreadPoolExecutor.CallerRunsPolicy executor がシャットダウンしていない場合に、execute メソッドの呼び出しで拒否されたタスクを直接実行する、拒否されたタスクのハンドラです。ThreadPoolExecutor.DiscardOldestPolicy executor がシャットダウンしていない場合に、もっとも古い未処理の要求を破棄して execute を再試行する、拒否されたタスクのハンドラです。ThreadPoolExecutor.DiscardPolicy 拒否されたタスクを通知なしで破棄する拒否されたタスクのハンドラです。下の2つ(DiscardOldestPolicy,DiscardPolicy)は
タスクを破棄されてしまうので、それで問題ない場合は使用しましょう!私が使用した時の要件では、絶対にタスクを破棄されてしまっては困るので上の2つ(AbortPolicy,CallerRunsPolicy)のどちらかを使う必要がありました。
私が考えた、この2つのポリシーのメリット・デメリットをまとめます
・CallerRunsPolicy メリット
タスクがあふれた場合は、親スレッドが処理を行うので効率よくタスクを処理してくれる。
デメリット
スレッドごとにトランザクションを管理している場合に、親スレッドのトランザクションが中断されてしまう可能性がある。
(親スレッドは、mainの最初に開始し、mainの最後にかならずトランザクションを終了させたい場合に、親がタスクの処理を行うとタスクの開始時にトランザクションを張ろうとしてネストしたトランザクションになってしまいます。
また、タスクの終了時にトランザクションを閉じるので、mainの最後でトランザクションを終了させることができなくなってしまいます)
・AbortPolicy メリット
親スレッドはタスクを処理しないので、CallerRunsPolicyのデメリットが発生しない。
(この点はDiscardOldestPolicy,DiscardPolicyでも同じことが言えます)
デメリットの項目とカブるが、タスクが溢れるようなロジックを作っていた場合に、例外が発生するため、間違に気付ける。
デメリット
タスクがあふれた場合は、例外が発生してしまう。
タスクをあふれさせないように、ロジックで制御する必要がある
CallerRunsPolicy、AbortPolicyをこのように分類し、最終的には両方とも使用しました。
トランザクションを考える必要がない場所ではCallerRunsPolicyを使い、そうでない場合はAbortPolicyを使いました。
■タスク登録方法ThreadPoolExecutorには、タスクとして扱える形式が2つ存在します。
1つ目がRunnableインタフェースを実装する方法、2つ目がCallableインタフェースを実装する方法です。
RunnableインタフェースとCallableインタフェースの簡単な違いは、Callableインタフェースの場合は戻り値を受け取れるということです。
使用用途によって分けるのもいいでしょう!
ただ、RunnableとCallableはタスクの登録方法が若干違うので設計するときは注意しましょう!
・Runnable1.コンストラクタ引数として渡す、BlockingQueueに格納しておく
2.executeメソッドに引数として渡す
3.submitメソッドに引数として渡す
・Callable1.submitメソッドに引数として渡す
Runnableの方がいろいろな方法でタスク登録できます。
使用する状況によって分けてみましょう!
こんな感じでThreadPoolExecutorにはいろいろな設定ができるので、
使用用途にあった使い方を吟味してしようしてください。