ThreadPoolExecutor スレッド プールをエレガントにカスタマイズする方法

1。概要

Java は、多くの場合、何らかのビジネスを処理するためにマルチスレッドを使用する必要があります。Thread を継承したり、Runnable インターフェイスを実装したりするだけでスレッドを作成することはお勧めできません。そうすると、スレッドの作成および破棄時にリソースの消費とスレッド コンテキストの切り替えの問題が必然的に発生します。同時に、スレッドを作成しすぎるとリソースが枯渇するリスクが生じる可能性があるため、スレッド タスクの管理を容易にするためにスレッド プールを導入する方が合理的です。

Java のスレッド プールに関連する関連クラスはすべて、jdk 1.5 以降の java.util.concurrent パッケージに含まれています。関連するいくつかのコア クラスとインターフェイスには、Executor、Executors、ExecutorService、ThreadPoolExecutor、FutureTask、Callable、Runnable などが含まれます。

JDK がスレッド プールを自動的に作成するためのいくつかの方法が、Executors ツール クラスにカプセル化されています。

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

使用されるコンストラクターは、

new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue())

corePoolSize=maxPoolSize、keepAliveTime=0 を設定します (このパラメータは現時点では効果がありません)、無制限のキュー、リクエストが多すぎる場合、タスクを無制限に配置できます (タスクの処理速度がタスクの送信速度とリクエストに追いつきません)累積)により、メモリが過剰に占有されたり、OOM 例外が直接発生したりする可能性があります。

  • newSingleThreadExector

使用されるコンストラクターは、

new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), var0)

基本的には newFixedThreadPool と同じですが、スレッド数が 1 に設定され、シングルスレッドになっており、欠点は newFixedThreadPool と同じです。

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

使用されるコンストラクターは、

new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue())

corePoolSize=0、maxPoolSize は非常に大きな数値です。キューは同期的に引き渡されます。つまり、常駐スレッド (コア スレッド) は維持されず、リクエストが行われるたびに新しいスレッドが直接作成されて処理されます。タスクとキュー バッファは使用されず、超過分は自動的に回復されます。スレッドの場合、maxPoolSize が Integer.MAX_VALUE に設定されているため、リクエストが多いときに作成されるスレッドが多すぎる可能性があり、リソース枯渇 OOM が発生します。

  • 新しいスケジュールされたスレッドプール

使用されるコンストラクターは、

new ThreadPoolExecutor(var1, 2147483647, 0L, TimeUnit.NANOSECONDS, new ScheduledThreadPoolExecutor.DelayedWorkQueue())

タイミングと定期的な実行をサポートしていますが、遅延キューが使用されており、欠点は newCachedThreadPool と同じであることに注意してください。

上記では、Executors ツール クラスを使用して作成されたスレッド プールには隠れた危険性があると述べましたが、この隠れた危険性を回避するにはどのように使用すればよいでしょうか? スレッド プールを使用する最もエレガントな方法はどのようにすればよいでしょうか? 実稼働環境で独自のスレッド プールを構成する方法は合理的ですか? 適切な薬を処方し、独自のスレッド ファクトリ クラスを構築し、主要なパラメータを柔軟に設定する必要があります。

2、ThreadPoolExecutor 类

スレッド プールをカスタマイズするには、ThreadPoolExecutor クラスを使用する必要があります。

ThreadPoolExecutor クラスのコンストラクター:

public ThreadPoolExecutor(int coreSize,int maxSize,long KeepAliveTime,TimeUnit unit,BlockingQueue queue,ThreadFactory factory,RejectedExectionHandler handler)

上記の構築方法には合計 7 つのパラメータがあり、これら 7 つのパラメータの意味は次のとおりです。

  • corePoolSize: コア スレッドの数は、スレッド プール内の常駐スレッドの数でもあります。スレッド プールが初期化されるとき、デフォルトではスレッドはありません。タスクが来ると、タスクを実行するためにスレッドが作成されます。
  • MaximumPoolSize: スレッドの最大数。一部の非コア スレッドは、コア スレッドの数に基づいて追加される場合があります。workQueue キューがいっぱいの場合にのみ、corePoolSize (スレッドの合計数) より多くのスレッドが作成されることに注意してください。スレッド プール内のスレッドは maxPoolSize を超えません)
  • keepAliveTime: アイドル時間が keepAliveTime を超えた場合、非コア スレッドは自動的に終了し、リサイクルされます。corePoolSize=maxPoolSize の場合、keepAliveTime パラメータは機能しないことに注意してください (非コア スレッドがないため)。
  • 単位: keepAliveTime の時間単位
  • workQueue: タスクを保存するために使用されるキュー。制限なしハンドオーバー、制限付きハンドオーバー、同期ハンドオーバーの 3 つのキュー タイプのいずれかになります。プール内のワーカー スレッドの数が corePoolSize よりも大きい場合、新しい受信タスクはキューに入れられます。
  • threadFactory: スレッドを作成するためのファクトリ クラス。デフォルトで Executors.defaultThreadFactory() を使用するか、guava ライブラリの ThreadFactoryBuilder を使用してスレッドを作成します。
  • handler: スレッド プールがタスクを受信し続けることができない場合 (キューが満杯で、スレッド数が MaximumPoolSize に達した場合) の飽和ポリシー。値は AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy です。

3. スレッドプール設定関連

3.1 スレッドプールサイズの設定

まず、この問題については、ニーズがコンピューティング集約型か IO 集約型かを明確にする必要があり、これを理解することによってのみ、スレッド プールの数をより適切に制限することができます。

  1. 計算量が多い

名前が示すように、このアプリケーションは多くの CPU コンピューティング リソースを必要としますが、マルチコア CPU の時代では、各 CPU コアが計算に参加し、CPU のパフォーマンスを最大限に活用できるようにする必要があります。サーバー構成は無駄ではない サーバー構成が非常に優れている場合、コンピューター上でシングルスレッド プログラムを実行することは非常に無駄なことになります。コンピューティング集約型アプリケーションの場合、作業は CPU コアの数に完全に依存するため、その利点を最大限に活用し、過度のスレッド コンテキストの切り替えを回避するには、理想的なソリューションは次のとおりです。

スレッド数 = CPU コア数 + 1、または CPU コア数 * 2 に設定できますが、JDK のバージョンと CPU 構成 (サーバーの CPU がハイパースレッディングを備えている) によっても異なります。

一般的にCPUを設定します※2。

  1. I/O集中型

現在開発しているのはWEBアプリケーションが多く、ネットワーク通信が多く、それだけでなくデータベースやキャッシュとのやり取りでもIOが発生し、IOが発生するとスレッドは待ち状態になります。 IO が終了すると、データの準備ができるまでスレッドは実行を継続しません。したがって、ここから、IO 集中型のアプリケーションの場合、スレッド プールにさらに多くのスレッドを設定できることがわかります。これにより、スレッドは IO を待機している間に他の作業を行うことができ、同時処理の効率が向上します。では、このスレッド プール内のデータ量は任意に設定できるのでしょうか? もちろんそうではありません。スレッド コンテキストの切り替えには代償が伴うことを覚えておいてください。現時点では、IO 集中型のアプリケーション向けに、次のような一連の式がまとめられています。

スレッド数 = CPU コア数 / (1-ブロッキング係数) このブロッキング係数は通常 0.8 ~ 0.9 であり、0.8 または 0.9 の場合もあります。

式を適用すると、デュアルコア CPU の場合、理想的なスレッド数は 20 です。もちろん、これは絶対的なものではなく、実際の状況や実際のビジネスに応じて調整する必要があります。final int poolSize = (int)(cpuCore/ (1-0.9))

ブロッキング係数に関しては、「Programming Concurrency on the JVM Mastering」または「Java Virtual Machine Concurrent Programming」に次のような一文があります。

ブロック要因については、最初に推測してみたり、繊細な分析ツールや Java を使用したりすることができます。

3.2 スレッドプール関連のパラメータ設定

  • 上限のない設定項目は選択しないようにしてください。

このため、Executor でスレッドを作成する方法を使用することはお勧めできません。

たとえば、Executors.newCachedThreadPool の設定と無制限のキューの設定により、予期せぬ状況によりスレッド プールでシステム例外が発生する可能性があり、その結果、スレッドの急増やタスク キューの継続的な拡張、およびシステム クラッシュや例外が発生することがあります。記憶力の枯渇。

この問題を回避するには、カスタム スレッド プールを使用することをお勧めします。これは、スレッド プール仕様を使用する最初の原則でもあります。

  • 次に、スレッドの数とスレッドのアイドル状態の回復時間を合理的に設定します。

頻繁なリサイクルと作成を避けるために、特定のタスクの実行サイクルと時間に応じて設定します スレッド プールを使用する目的はシステムのパフォーマンスとスループットを向上させることですが、システムの安定性も考慮する必要があり、そうしないと予期せぬ問題が発生しますとても面倒なことになります!

  • 3 番目に、実際のシナリオに基づいて、自分に適用される拒否戦略を選択します。

JDK がサポートする自動補正メカニズムをいじらないで、補正を行ってください。カスタムの拒否戦略を使用して、収益をカバーするようにしてください。

  • 4 番目に、スレッド プール拒否戦略、カスタム拒否戦略は RejectedExecutionHandler インターフェイスを実装できます。

JDK に付属する拒否戦略は次のとおりです。

AbortPolicy: 例外をスローすると、システムが正常に動作しなくなります。
CallerRunsPolicy: スレッド プールが閉じられていない限り、このポリシーは現在破棄されているタスクを呼び出し側スレッドで直接実行します。
DiscardOldestPolicy: 最も古いリクエストを破棄し、現在のタスクを再度送信してみます。
DiscardPolicy: 処理を行わずに処理できないタスクを破棄します。

4. フックの使用

フックを使用してスレッド プールの実行トラックを残します。

ThreadPoolExecutor は、保護された型をオーバーライドできるフック メソッドを提供し、タスクが実行される前にユーザーが何かをできるようにします。これを使用して、ThreadLocal の初期化、統計の収集、ログの記録などの操作を実装できます。このようなフックは beforeExecute と afterExecute です。タスクの実行時にユーザーが再終了などのロジックを挿入できるようにするために使用できるフックもあります。

フック メソッドが失敗すると、内部ワーカー スレッドの実行が失敗するか中断されます。

beforeExecute と afterExecute を使用して、スレッドの前後の実行状況を記録したり、実行完了後のステータスを ELK などのログ システムに直接記録したりできます。

5. スレッドプールを閉じる

スレッド プールが参照されなくなり、ワーカー スレッドの数が 0 になると、スレッド プールは終了します。また、shutdown を呼び出してスレッド プールを手動で終了することもできます。shutdown の呼び出しを忘れた場合は、スレッド リソースを解放するために、keepAliveTime とallowCoreThreadTimeOut を使用して目標を達成することもできます。

もちろん、安全な方法は、仮想マシンの Runtime.getRuntime().addShutdownHook メソッドを使用して、スレッド プールの shutdown メソッドを手動で呼び出すことです。

6. 最適化できる項目

6.1 スレッドプール内のスレッドをデーモンとして設定する

通常の状況では、スレッド プールが閉じられた後、スレッド プール内のスレッドは自動的に終了します。ただし、スレッド自身または直接 new Thread() であるふりをする一部のスレッドでは、プロセスの終了がブロックされます。

Javaのプロセス終了判定方法では、デーモンスレッドのみが存在する場合、プロセスは正常に終了します。したがって、これらの非メインスレッドをデーモンとして設定すること、つまり、プロセスの終了をブロックしないことをお勧めします。

6.2 スレッドの正しい名前

スレッド プールを使用する場合、スレッドの作成方法を制御するために ThreadFactory オブジェクトが一般的に受け入れられます。Java に付属の ExecutorService では、このパラメータが設定されていない場合、デフォルトの DefaultThreadFactory が使用されます。その結果、スレッド スタック リストに一連の pool-x-thread-y が表示されますが、実際に jstack を使用すると、これらの各スレッドが属するグループや特定の関数は表示されません。

6.3 使用できなくなった定期タスクを破棄する

一般に、Java に付属の ScheduledThreadPoolExecutor を使用して、scheduleAtFixedRate およびscheduleWithFixedDelay を呼び出すと、タスクが定期的 (期間) に設定されます。スレッド プールが閉じられると、これらのタスクは (デフォルトで) 直接破棄できますが、スケジュールを使用して長期タスクを追加する場合、それは定期的なタスクではないため、スレッド プールは対応するスレッドを閉じません。

たとえば、Spring システムの TriggerTask (CronTask を含む) は、タスクのスケジューリングのタイミングに使用されます。タスクは最終的にスケジュールによってスケジュールされ、1 つのタスクが完了すると、次のタスクがスケジュールによって再度実行されます。このメソッドは期間ではないとみなされるため、このスケジュール メソッドを使用すると、コンテナが閉じられるときに shutdown メソッドが実行されますが、対応する基になる ScheduledExecutorService は依然として正常に閉じられません (すべての状態は設定されていますが)。最終的な効果は、スレッド プールがすでにシャットダウン状態にあるにもかかわらず、スレッドがまだ実行中である (状態は待機タスクである) ことです。

このメソッドを解決するために、Java は追加の設定パラメータexecuteExistingDelayedTasksAfterShutdownを提供します。この値はデフォルトでtrueです。つまり、シャットダウン後も実行されます。スレッド プールを定義するときに false に設定できます。つまり、スレッド プールが閉じられると、これらの遅延タスクは実行されなくなります。

おすすめ

転載: blog.csdn.net/qq_43842093/article/details/131262789