1. スレッド プールを使用する理由
Web サーバー、データベース サーバー、ファイル サーバー、メール サーバーなどの多くのサーバー アプリケーションは、リモート ソースからの多数の短いタスクを処理することを目的としています。リクエストは、ネットワーク プロトコル (HTTP、FTP、POP など)、JMS キュー、またはデータベースのポーリングなど、何らかの方法でサーバーに到達します。リクエストの到着方法に関係なく、サーバー アプリケーションでは、単一タスクの処理時間は非常に短いのに、リクエストの数が膨大になることがよくあります。
サーバー アプリケーションを構築するための単純なモデルは、リクエストが到着するたびに新しいスレッドを作成し、その新しいスレッドでリクエストを処理することです。実際、このアプローチはプロトタイピングには問題なく機能しますが、この方法で実行されるサーバー アプリケーションを展開しようとすると、このアプローチの重大な欠点が明らかになります。リクエストごとのスレッドのアプローチの欠点の 1 つは、リクエストごとに新しいスレッドを作成するのにコストがかかることです。リクエストごとに新しいスレッドを作成するサーバーは、スレッドの作成と破棄に多くの時間を費やし、より多くの時間とシステムを消費します。実際のユーザー要求の処理に費やされるリソースよりも多くのリソースが使用されます。
スレッドの作成と破棄のオーバーヘッドに加えて、アクティブなスレッドはシステム リソースも消費します。 JVM で作成するスレッドが多すぎると、システムがメモリ不足になったり、過度のメモリ消費による「オーバースイッチ」が発生したりする可能性があります。リソースの枯渇を防ぐために、サーバー アプリケーションには、一度に処理できるリクエストの数を制限する何らかの方法が必要です。
スレッド プールは、スレッド ライフ サイクルのオーバーヘッド問題とリソース不足の問題の解決策を提供します。複数のタスクにスレッドを再利用することにより、スレッド作成のオーバーヘッドが複数のタスクに分散されます。利点は、リクエストが到着したときにスレッドがすでに存在しているため、スレッドの作成によって生じる遅延も不用意に解消されることです。こうすることで、リクエストを即座に処理できるため、アプリケーションの応答性が向上します。さらに、スレッド プール内のスレッドの数を適切に調整することにより、つまりリクエストの数が一定のしきい値を超えた場合、他の受信リクエストは、それらを処理するためのスレッドが取得されるまで強制的に待機されるため、リソースの不足を防ぐことができます。
2. スレッド プールを使用するリスク
スレッド プールはマルチスレッド アプリケーションを構築するための強力なメカニズムですが、その使用にはリスクがないわけではありません。スレッド プールを使用して構築されたアプリケーションは、同期エラーやデッドロックなど、他のマルチスレッド アプリケーションが影響しやすいすべての同時実行リスクの影響を受けやすくなります。また、プール関連のデッドロックなど、スレッド プールに特有の他のいくつかのリスクにも影響されます。 , リソース不足とスレッド漏れ。
2.1 デッドロック
マルチスレッド アプリケーションにはデッドロックの危険があります。プロセスまたはスレッドのグループのそれぞれが、グループ内の別のプロセスによってのみ引き起こされる可能性のあるイベントを待機しているとき、そのプロセスまたはスレッドのグループはデッドロックしていると言います。デッドロックの最も単純なケースは、スレッド A がオブジェクト X の排他ロックを保持し、オブジェクト Y のロックを待機しているのに対し、スレッド B はオブジェクト Y の排他ロックを保持しているが、オブジェクト X のロックを待機していることです。ロックの待機を解除する何らかの方法がない限り (Java ロックはこの方法をサポートしていません)、デッドロックされたスレッドは永久に待機することになります。
マルチスレッド プログラムにはデッドロックのリスクがありますが、スレッド プールではデッドロックの別の可能性が生じます。つまり、すべてのプール スレッドが、キュー内でブロックされて待機している別のタスクを実行しますが、その結果を実行するタスクです。占有されていないスレッドがないため、タスクを実行できません。これは、スレッド プールを使用して、多くの相互作用するオブジェクトが関与するシミュレーションを実装する場合に発生します。シミュレートされたオブジェクトは相互にクエリを送信でき、クエリ オブジェクトが同期的に応答を待機している間に、これらのクエリはキューに入れられたタスクとして実行されます。
2.2 リソース不足
スレッド プールの利点の 1 つは、他の代替スケジューリング メカニズム (そのうちのいくつかはすでに説明しました) と比べて一般的に非常に優れたパフォーマンスを発揮することです。ただし、これはスレッド プール サイズが適切に調整されている場合にのみ当てはまります。スレッドは、メモリやその他のシステム リソースを含む大量のリソースを消費します。 Thread オブジェクトに必要なメモリに加えて、各スレッドには 2 つの実行呼び出しスタックが必要ですが、これは大きくなる可能性があります。さらに、JVM は Java スレッドごとにネイティブ スレッドを作成する場合があり、これらのネイティブ スレッドは追加のシステム リソースを消費します。最後に、スレッド間の切り替えによるスケジューリングのオーバーヘッドは小さいですが、スレッドが多数ある場合、コンテキストの切り替えがプログラムのパフォーマンスに重大な影響を与える可能性があります。
スレッド プールが大きすぎる場合、それらのスレッドによって消費されるリソースがシステムのパフォーマンスに重大な影響を与える可能性があります。スレッド間の切り替えは時間を無駄にします。また、実際に必要な以上のスレッドを使用すると、他のタスクでより効率的に使用される可能性のあるリソースをプール スレッドが消費するため、リソース枯渇の問題が発生する可能性があります。スレッド自体によって使用されるリソースに加えて、リクエストの処理で実行される作業には、JDBC 接続、ソケット、ファイルなどの他のリソースが必要になる場合があります。これらもリソースが限られており、同時リクエストが多すぎると、JDBC 接続を割り当てられないなどの障害が発生する可能性があります。
2.3 同時実行エラー
スレッド プールとその他のキューイング メカニズムは wait() メソッドと Notice() メソッドの使用に依存していますが、どちらも使用するのが困難です。正しくコーディングされていない場合、通知が失われ、キューに処理すべき作業があるにもかかわらず、スレッドがアイドル状態のままになる可能性があります。これらの方法を使用する場合は、細心の注意を払う必要があります。代わりに、util.concurrent パッケージなど、動作することがすでにわかっている既存の実装を使用することをお勧めします。
2.4 スレッド リーク
さまざまなタイプのスレッド プールにおける重大なリスクはスレッド リークです。これは、タスクを実行するためにスレッドがプールから削除され、タスクの完了後にスレッドがプールに戻されない場合に発生します。 。スレッド リークが発生する状況の 1 つは、タスクが RuntimeException またはエラーをスローした場合です。プール クラスがそれらをキャッチしない場合、スレッドは単に終了し、スレッド プールのサイズは永続的に 1 つ減ります。これが何度も発生すると、最終的にはスレッド プールが空になり、タスクを処理できるスレッドがなくなるためシステムが停止します。
一部のタスクは特定のリソースまたはユーザーからの入力を永遠に待機する可能性があり、これらのリソースが利用可能になるとは限りません。そのようなタスクは永久に停止し、これらの停止したタスクもスレッドと同じ問題を引き起こします。リーク。スレッドがそのようなタスクによって永続的に消費される場合、そのスレッドは事実上プールから削除されます。このようなタスクの場合は、独自のスレッドのみを与えるか、限られた時間だけ待機させる必要があります。
2.5 リクエストの過負荷
リクエストだけでサーバーに負荷がかかる可能性があります。このシナリオでは、実行のためにキューに入れられたタスクがシステム リソースを大量に消費し、リソース枯渇を引き起こす可能性があるため、すべての受信リクエストをワーク キューにキューに入れることは望ましくありません。この状況で何をするかはユーザー次第です。場合によっては、単純にリクエストを放棄し、上位レベルのプロトコルを利用して後でリクエストを再試行することも、サーバーが一時的に停止していることを示す応答で応答することもできます。リクエストを拒否するのに忙しいです。
3. スレッド プールを効果的に使用するためのガイドライン
いくつかの簡単なガイドラインに従う限り、スレッド プールはサーバー アプリケーションを構築する非常に効果的な方法になります。
他のタスクの結果を同期的に待機しているタスクをキューに入れないでください。タスク。これにより、前述のデッドロックの形式が発生する可能性があります。この状態では、すべてのスレッドが非常にビジーであるために実行できないキューに入れられたタスクの結果を待っているタスクによってすべてのスレッドが占有されます。
長時間かかる可能性のある操作にプールされたスレッドを使用する場合は注意してください。 I/O などのリソースが完了するまでプログラムが待機する必要がある場合は、最大待機時間を指定し、その後タスクを無効にするか、後で実行するために再度キューに入れるかを指定します。そうすることで、正常に完了する可能性が高いタスクへのスレッドを解放することで、最終的にある程度の進捗が得られることが保証されます。
タスクを理解します。スレッド プールのサイズを効果的に設定するには、キューに入れられているタスクとその実行内容を理解する必要があります。 CPU に依存しているのでしょうか? I/Oバウンドですか?あなたの答えは、アプリケーションの調整方法に影響します。特性が大きく異なるさまざまなタスク クラスがある場合、それに応じて各プールを調整できるように、さまざまなタスク クラスに複数のワーク キューを用意することが合理的である可能性があります。
4. スレッド プール サイズの設定
スレッド プールのサイズを調整するのは、基本的に、スレッドが少なすぎるかスレッドが多すぎるという 2 種類のエラーを回避するためです。幸いなことに、ほとんどのアプリケーションでは、多すぎることと少なすぎることの間のマージンがかなり広いです。
覚えておいてください: アプリケーションでスレッドを使用すると、I/O などの遅い操作を待っていても処理を続行できること、および複数のプロセッサを活用できることという 2 つの主な利点があります。 N 個のプロセッサを備えたマシンの計算制約下で実行されているアプリケーションでは、スレッド数が N に近づくにつれてスレッドを追加すると全体の処理能力が向上する可能性がありますが、スレッド数が N を超えると追加のスレッドを追加しても効果はありません。実際、スレッドが多すぎると、追加のコンテキスト切り替えオーバーヘッドが発生するため、パフォーマンスが低下する可能性さえあります。
スレッド プールの最適なサイズは、使用可能なプロセッサの数とワーク キュー内のタスクの性質によって異なります。 N 個のプロセッサ (すべてが計算タスク) を備えたシステム上にワーク キューが 1 つだけある場合、通常、スレッド プールに N または N+1 のスレッドがあるときに CPU 使用率が最大になります。
I/O が完了するまで待機する必要がある可能性のあるタスク (ソケットから HTTP リクエストを読み取るタスクなど) については、すべてのスレッドが動作しているわけではないため、プール サイズを使用可能なプロセッサーの数を超えるようにする必要があります。時間。プロファイリングを使用すると、一般的なリクエストの待機時間 (WT) とサービス時間 (ST) の比率を推定できます。この比率を WT/ST と呼ぶ場合、N 個のプロセッサを備えたシステムの場合、プロセッサを最大限に活用するには、約 N*(1+WT/ST) 個のスレッドをセットアップする必要があります。
スレッド プールのサイジング プロセスで考慮すべき点は、プロセッサーの使用率だけではありません。スレッド プールが大きくなるにつれて、スケジューラ、使用可能なメモリ、またはソケットの数、開いているファイル ハンドル、データベース接続などのその他のシステム リソースに制限が生じる可能性があります。
5. 一般的に使用されるいくつかのスレッド プール
5.1 newCachedThreadPool
スレッド プールの長さが処理の必要性を超えた場合、アイドル状態のスレッドを柔軟にリサイクルできます。作成した。
このタイプのスレッド プールの特徴は次のとおりです:
• 作成されるワーカー スレッドの数にほとんど制限がないため (実際には制限があり、数は Integer.MAX_VALUE です)、スレッドを追加できます。スレッドプールに柔軟に対応します。
• スレッド プールにタスクが長期間送信されなかった場合、つまり、ワーカー スレッドが指定された時間 (デフォルトは 1 分) アイドル状態になった場合、ワーカー スレッドは自動的に終了します。終了後に新しいタスクを送信すると、スレッド プールはワーカー スレッドを再作成します。
• CachedThreadPool を使用する場合は、タスクの数の制御に注意する必要があります。そうしないと、同時に多数のスレッドが実行されるため、システムが麻痺する可能性があります。
サンプルコードは以下の通りです:
package test; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolExecutorTest { public static void main(String[] args) { ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); for (int i = 0; i < 10; i++) { final int index = i; try { Thread.sleep(index * 1000); } catch (InterruptedException e) { e.printStackTrace(); } cachedThreadPool.execute(new Runnable() { public void run() { System.out.println(index); } }); } } }
5.1 newFixedThreadPool
指定された数のワーカースレッドを持つスレッドプールを作成します。タスクが送信されるたびにワーカー スレッドが作成され、ワーカー スレッドの数がスレッド プールの初期最大数に達すると、送信されたタスクはプール キューに格納されます。
FixedThreadPool は典型的な優れたスレッド プールで、プログラムの効率を向上させ、スレッド作成時のオーバーヘッドを節約するという利点があります。ただし、スレッド プールがアイドル状態のとき、つまりスレッド プールに実行可能なタスクがないときは、ワーカー スレッドは解放されず、特定のシステム リソースも占有します。
サンプルコードは以下の通りです:
package test; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolExecutorTest { public static void main(String[] args) { ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3); for (int i = 0; i < 10; i++) { final int index = i; fixedThreadPool.execute(new Runnable() { public void run() { try { System.out.println(index); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } }); } } }
スレッドプールサイズが3なので、各タスクはインデックス出力後2秒間スリープするため、2秒ごとに3つの数字が出力されます。
固定長スレッド プールのサイズは、Runtime.getRuntime().availableProcessors() などのシステム リソースに応じて設定するのが最適です。
5.1 newSingleThreadExecutor
シングルスレッド Executor を作成します。つまり、タスクを実行するための唯一のワーカー スレッドのみを作成し、すべてのタスクが指定された順序 (FIFO、FIFO) になるようにします。 LIFO、優先レベル) の実行。このスレッドが異常終了した場合は、順次実行を保証するために別のスレッドに置き換えられます。単一ワーカー スレッドの最大の特徴は、タスクが順番に実行され、同時に複数のスレッドがアクティブになることがないことを保証できることです。
サンプルコードは以下の通りです:
package test; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolExecutorTest { public static void main(String[] args) { ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); for (int i = 0; i < 10; i++) { final int index = i; singleThreadExecutor.execute(new Runnable() { public void run() { try { System.out.println(index); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } }); } } }
5.1 newScheduleThreadPool
固定長のスレッドプールを作成し、スケジュールされた定期的なタスクの実行をサポートし、スケジュールされた定期的なタスクの実行をサポートします。
3 秒間実行を遅延します。 遅延実行のサンプル コードは次のとおりです。
package test; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class ThreadPoolExecutorTest { public static void main(String[] args) { ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5); scheduledThreadPool.schedule(new Runnable() { public void run() { System.out.println("delay 3 seconds"); } }, 3, TimeUnit.SECONDS); } }
は、1 秒の遅延後に 3 秒ごとに実行されることを意味します。 通常の実行のサンプル コードは次のとおりです。
上記の記事は、一般的に使用されるいくつかの Java スレッド プールの比較について簡単に説明したものであり、編集者が共有するすべての内容が参考になることを願っています。また、PHP 中国語 Web サイトをサポートしていただければ幸いです。 Java で一般的に使用されるいくつかのスレッド プールの比較に関するその他の関連記事については、PHP 中国語 Web サイトに注目してください。