Javaエフェクタによるスレッドプールの実装


JSR 315-servlet仕様3.0のレポートを作成するとき、非同期servletを理解する鍵の一つは、Javaにおける非同期処理メカニズムをまず理解することにあることに気づきました.Javaの非同期処理の重要なコンポーネントであるため、すぐにエフェクタ(Executor)とエフェクタサービス(ExecutorService)に陥りました.このブログでは、このテーマについて把握したことをまとめます.
いくつかの概念
≪タスク|Task|emdw≫:特定の時点で開始された一連の作業を表し、いくつかのアクティビティまたは計算を行い、終了する小さな独立したアクティビティとして定義されます.1つのウェブサーバでは、各受信された独立したリクエストがこの定義を満たします.Javaでは、タスクの体現はRunnableまたはCallableのインスタンスである.
スレッド:タスクの実行例と考えられます.タスクが一連の完了する必要がある作業を表す場合、スレッドはそのタスクの実際の実行を表す.JavaではスレッドはThreadのインスタンスとして表現される.
≪同期処理|Synchronize Process|emdw≫:タスクがプライマリ・スレッドの実行中に完了する必要がある場合に発生します.すなわち、プライマリ・プログラムは、現在のタスクの実行が終了するまで、独自のプロセスを処理し続ける必要があります.
非同期処理:プライマリ・スレッドがタスクの処理を分離された独立したスレッドに委任する場合.このスレッドはタスクに関する処理を担当し、居住スレッドはメインプログラムが次に行うことを処理します.
≪スレッド・プール|Thread Pool|emdw≫:割り当てられた作業を待つ1つ以上のスレッドを表します.スレッドプールは、多くのメリットをもたらします.まず、プール内のスレッドが無から再作成されるたびに多重化されるのではなく、スレッドの作成と破棄に伴うシステムオーバーヘッドを低減します.次に、システム内のアクティブなスレッドの数を制御することができ、サーバのメモリと計算の負担を軽減します.最後に、スレッド管理という難しい問題をスレッドプールに委任することで、プログラムを簡素化できます.
ここでは、役割を果たす3つの重要なメカニズムを指摘する必要があります.タスクの到来(一連の作業の完了を要求するもの)を処理し、受信機にタスクを提出し、各タスクの実際の実行を行う必要があります.JavaのExecutorフレームワークは、後の2つのメカニズムを分離-コミットと処理します.
要求の到来は通常、プログラムの制御範囲内ではありません.これは、お客様からの要求に依存します.1つの要求の送信は、通常、要求されたタスクがタスクに入るキューに追加され、処理は、スレッドプールに空き待機するスレッドに1つのタスクを割り当てるために処理される.
Java 5.0とスレッドプールJava 5.0は、独自のスレッドプール実装-ExecutorとExecutorServiceインタフェースを導入しています.これにより、自分のプログラムでスレッドプールを使用することが容易になります.
Executorは、アプリケーションのタスクの考慮に便利な抽象を提供します.スレッドの側面から考慮する必要はありません.アプリケーションはRunnableのインスタンスを簡単に処理し、Executorに渡して処理するだけです.
ExecutorServiceインタフェースは、スレッドプール内のスレッドを管理するためのライフサイクルメソッドを追加する非常にシンプルなExecutorインタフェースを継承します.たとえば、プール内のすべてのスレッドをオフにすることができます.
また、Executorでは、単純なタスクをプール内のスレッドにコミットできます.ExecutorServiceでは、タスクのセットをコミットしたり、Futrureオブジェクトを取得してタスクの実行状況を追跡したりすることもできます.
RunnableおよびCallableExecutorフレームワークは、RunnableインスタンスまたはCallableインスタンスを使用する一連のタスクを表します.Runnableのrun()メソッドの制限は,戻り値もchecked異常も投げないことである.Callableは強化版で、call()メソッドを定義して、いくつかの計算値の戻りを許可し、必要であれば異常を投げ出すことができます.
タスクを制御するには、FutureTaskクラスを使用してタスクの詳細を取得できます.CallableまたはRunnableのインスタンスをパッケージ化できます.ExecuterServiceのsubmit()メソッドの戻り値を呼び出すことでFutureTaskインスタンスを取得したり、execute()メソッドを呼び出す前に手動でFutureTaskにタスクをパッケージしたりすることができます.
FutureTaskの例は、Futureインタフェースを実装しているため、実行中のタスクを監視したり、タスクをキャンセルしたり、実行結果を取得したりすることができます(Callableのcall()メソッドに戻り値があるように).
ThreadPoolExecutorで最も一般的なExecutorService実装はThreadPoolExecutorです.
タスクはRunnableのインスタンスとしてThreadPoolExecutorに提出され、後者は実際の処理を担当しますが、あなたのアプリケーションはこの抽象的な背後で何が起こっているのか気にする必要はありません.
ThreadPoolExecutorの定義は次のとおりです.
  • スレッドプール(最大スレッドと最小スレッドの数を定義).
  • ワークキュー:このキューは、スレッドプール内のスレッドに順次割り当てられるコミットされたタスクを有します.主に2つのタイプのキュー-境界と境界がありません.境界キューにタスクを追加することは、常に正常に追加されますが、固定容量のLinkedBlockingQueueなどの境界キューは、保留中のタスクが最大容量に達したときに新しいタスクの追加を拒否します.
  • 拒否されたタスクをどのように処理するかを定義するプロセッサ(飽和ポリシー):タスクがキューに追加できない場合、スレッドプールは登録された拒否プロセッサを呼び出して、何が起こるかを決定します.デフォルトの拒否ポリシーは、単純にRejectedExecutionException実行時の例外を投げ出し、プログラムによってその例外をキャプチャして処理することです.他にも、DiscardPolicyなどのポリシーがあります.これは、通知なしにタスクを黙々と破棄します.
  • スレッドファクトリ:デフォルトでは、ThreadPoolExecutorエフェクタが構築した新しいスレッドには、スレッド優先度などの特定のプロパティと、スレッドプールの数、スレッドプール内のスレッド数に基づいて決定されるスレッド名があります.カスタムファクトリを使用して、これらのデフォルト値を書き換えることができます.

  • エフェクタを使用するアルゴリズム
    1.エフェクタを作成するには、まずグローバル環境でExecutorまたはExecutorServiceのインスタンス(servletコンテナの下のアプリケーションのコンテキストなど)を作成します.
    Executorsクラスでは、ExecutorServiceを作成する静的ファクトリメソッドが多数用意されています.たとえば、newFixedThreadPool()は、境界のないキューと固定スレッドの数を持つThreadPoolExecutorインスタンスを返します.NewCachedThreadPool()メソッドは、無境界キューと無境界スレッドの数を持つThreadPoolExecutorインスタンスを返します.後者の場合、空きスレッドがあれば、そのスレッドは多重化される.空きスレッドがない場合は、新しいスレッドが作成され、スレッドプールに追加されます.一定時間を超えて常にアイドル状態にあるスレッドは、スレッドプールから移動されます.
    private static final Executor executor = Executors.newFixedThreadPool(10);

    これらの便利な方法を使用しないと、自分で定義したThreadPoolExecutorを使用するのが適切であることに気づくかもしれません.その多くの構造子を使用します.
    private static final Executor executor = new ThreadPoolExecutor(10, 10, 50000L,   TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(100));

    このコンストラクションサブは、サイズ100の境界キュー、スレッドプール固定サイズ10のThreadPoolExecutorを作成します.
    2.1つ以上のタスクを作成するには、RunnableまたはCallableインスタンスによって実行される1つ以上のタスクを作成する必要があります.
    3.エグゼクティブにタスクをコミットExecuterServiceがあれば、submit()またはexecute()メソッドを使用してタスクをコミットし、スレッドプールからの空きスレッドを使用してタスクを列に並べて実行できます.
    4.タスク実行器は、スレッドプールとキューの管理に加えて、タスクの実行も管理します.ここで具体的に何が起こるかは、スレッドプールのサイズ制限、空きスレッドの数、キューの境界に依存します.
    通常、スレッドプールに定義された最小スレッド数よりも少ないスレッドがある場合、新しいスレッドは、その数の制限に達するまでキュー内のタスクを処理するために作成されます.
    プール内のスレッド数が構成の最小スレッド数を超えると、スレッドプールはより多くのスレッドを作成しなくなり、1つのスレッドが空き出すまでタスクキューに入れられます.キューがいっぱいになったら、新しいスレッドを起動してこの新しいタスクを処理しなければなりません.
    プール内のスレッド数が構成の最大スレッド数に達した場合、スレッドプールは新しいスレッドを開始しません.新しくコミットされたタスクは、タスクキューに追加されるか、キューがいっぱいで拒否されます.
    スレッドプール内のスレッドは、キューを監視し続け、タスクの実行を取得します.構成の最大アイドル時間を超えるスレッドが終了します.
    5.エフェクタのクローズプログラムがクローズされている間、エフェクタのshutdown()メソッドを呼び出してクローズします.暴利閉鎖か優雅閉鎖かを選ぶことができます.
    参考資料
  • Goetz等著『Java同時プログラミング実践』
  • OaksとWong著『Javaスレッド』
  • テキストリンク:http://www.softwareengineeringsolutions.com/blogs/2010/07/21/implementing-thread-pools-using-java-executors/.