ホームページ  >  記事  >  Java  >  Java スレッド プールのいくつかの実装方法とよくある質問への回答

Java スレッド プールのいくつかの実装方法とよくある質問への回答

黄舟
黄舟オリジナル
2017-01-20 11:15:171424ブラウズ

以下のエディターでは、いくつかの実装方法と、Java スレッド プールに関するよくある質問への回答が提供されます。編集者がとても良いと思ったので、参考として共有したいと思います。編集者をフォローして見てみましょう

仕事にはスレッドが関与することがよくあります。たとえば、一部のタスクは非同期実行のためにスレッドに渡されることがよくあります。または、サーバー プログラムはリクエストごとに個別のスレッド処理タスクを作成します。スレッドの外部 (使用するデータベース接続など)。これらの作成、破棄、または開閉操作は、システムのパフォーマンスに大きな影響を与えます。そこで、「プール」の有用性がクローズアップされます。

1. スレッド プールを使用する理由

セクション 3.6.1 で紹介した実装では、新しいワーカー スレッドが各顧客に割り当てられます。ワーカー スレッドとクライアント間の通信が終了すると、このスレッドは破棄されます。この実装には次の欠点があります:

•サーバーの作成と破棄作業のオーバーヘッド (費やされる時間とシステム リソースを含む) が大きい。この項目は説明の必要はありませんが、「スレッドの作成プロセス」を確認できます。マシン自体によって実行される作業に加えて、インスタンス化と起動も行う必要があり、これらにはすべてスタック リソースが必要です。

•スレッドの作成と破棄のオーバーヘッドに加えて、アクティブなスレッドはシステム リソースも消費します。 これはスタック リソースの消費を考慮して、データベース接続数の適切な値を設定することも考慮すべきだと思います。

•スレッドの数が固定され、各スレッドのライフサイクルが長い場合、スレッドの切り替えも比較的固定されます。オペレーティング システムが異なればスイッチング サイクルも異なり、通常は約 20 ミリ秒です。ここで言う切り替えとは、jvm と基礎となるオペレーティング システムのスケジューリングに基づいてスレッド間で CPU 使用権を転送することです。スレッドの作成と破棄が頻繁に行われる場合、スレッドは頻繁に切り替えられます。これは、スレッドが破棄された後、スレッドが実行できるように、すでに準備ができているスレッドに使用権を与える必要があるためです。この場合、スレッド間の切り替えはシステムの固定切り替えサイクルに従わなくなり、スレッド切り替えのコストは作成と破棄のコストよりもさらに大きくなります。

相対的に言えば、スレッド プールを使用する場合、いくつかのスレッドが事前に作成され、ワー​​ク キューからタスクを継続的に取り出して実行します。ワーカー スレッドはタスクの実行を終了すると、ワーク キュー内の別のタスクの実行を続けます。利点は次のとおりです:

•作成と破棄の数が減り、各ワーカー スレッドを常に再利用でき、複数のタスクを実行できます。

•システムリソースの過剰な消費によるシステムクラッシュを防ぐために、システムの処理能力に応じてスレッドプール内のスレッド数を簡単に調整できます。

2. スレッド プールの簡単な実装

以下は私が書いた簡単なスレッド プールです。これも Java ネットワーク プログラミングの本から直接入力したものです

package thread;
 
import java.util.LinkedList;
 
/**
 * 线程池的实现,根据常规线程池的长度,最大长度,队列长度,我们可以增加数目限制实现
 * @author Han
 */
public class MyThreadPool extends ThreadGroup{
  //cpu 数量 ---Runtime.getRuntime().availableProcessors();
  //是否关闭
  private boolean isClosed = false;
  //队列
  private LinkedList<Runnable> workQueue;
  //线程池id
  private static int threadPoolID;
  private int threadID;
  public MyThreadPool(int poolSize){
    super("MyThreadPool."+threadPoolID);
    threadPoolID++;
    setDaemon(true);
    workQueue = new LinkedList<Runnable>();
    for(int i = 0;i<poolSize;i++){
      new WorkThread().start();
    }
  }
  //这里可以换成ConcurrentLinkedQueue,就可以避免使用synchronized的效率问题
  public synchronized void execute(Runnable task){
    if(isClosed){
      throw new IllegalStateException("连接池已经关闭...");
    }else{
      workQueue.add(task);
      notify();
    }
  }
   
  protected synchronized Runnable getTask() throws InterruptedException {
    while(workQueue.size() == 0){
      if(isClosed){
        return null;
      }
      wait();
    }
    return workQueue.removeFirst();
  }
   
  public synchronized void close(){
    if(!isClosed){
      isClosed = true;
      workQueue.clear();
      interrupt();
    }
  }
   
  public void join(){
    synchronized (this) {
      isClosed = true;
      notifyAll();
    }
    Thread[] threads = new Thread[activeCount()];
    int count = enumerate(threads);
    for(int i = 0;i<count;i++){
      try {
        threads[i].join();
      } catch (Exception e) {
      }
    }
  }
   
  class WorkThread extends Thread{
    public WorkThread(){
      super(MyThreadPool.this,"workThread"+(threadID++));
      System.out.println("create...");
    }
    @Override
    public void run() {
      while(!isInterrupted()){
        System.out.println("run..");
        Runnable task = null;
        try {
          //这是一个阻塞方法
          task = getTask();
           
        } catch (Exception e) {
           
        }
        if(task != null){
          task.run();
        }else{
          break;
        }
      }
    }
  }
}

このスレッド プールは主にワーク キューといくつかの事前作成されたスレッドを定義します。実行メソッドが呼び出されている限り、タスクをスレッドに送信できます。

タスクがない場合、新しいタスクが入ってきて起動されるまで、後続のスレッドは getTask() でブロックされます。

join と close の両方を使用して、スレッド プールを閉じることができます。違いは、join ではキュー内のタスクの実行が終了するのに対し、close ではキューが即座にクリアされ、すべてのワーカー スレッドが中断されることです。 close()のinterrupt()は、ThreadGroup内の子スレッドのそれぞれのinterrupt()を呼び出すことと同等であるため、スレッドが待機またはスリープ状態にある場合、InterruptExceptionがスローされます

テストクラスは次のとおりです:

public class TestMyThreadPool {
  public static void main(String[] args) throws InterruptedException {
    MyThreadPool pool = new MyThreadPool(3);
    for(int i = 0;i<10;i++){
      pool.execute(new Runnable() {
        @Override
        public void run() {
          try {
            Thread.sleep(1000);
          } catch (InterruptedException e) {
          }
          System.out.println("working...");
        }
      });
    }
    pool.join();
    //pool.close();
  }
}

3. jdk クラス ライブラリ

java によって提供されるスレッド プールは、独自の実装よりも堅牢かつ効率的で、より強力なスレッド プールの実装を提供します。

クラス図は次のとおりです:

このタイプのスレッド プールについては、先輩たちがすでに十分な説明を行っています。 Baidu の Java スレッド プールには非常に詳細な例とチュートリアルが記載されているため、ここでは詳しく説明しません。

4. スレッドプールへの Spring インジェクション

Spring フレームワークを使用する場合、Java が提供するメソッドを使用してスレッド プールを作成すると、マルチスレッド アプリケーションでの管理が非常に不便になり、準拠しません。バネを使うという私たちのアイデアに。 (Spring は静的メソッドを通じて注入できますが)

実際、Spring 自体も優れたスレッド プール実装を提供します。このクラスは ThreadPoolTask​​Executor と呼ばれます。

春の構成は次のとおりです:

<bean id="executorService" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
    <property name="corePoolSize" value="${threadpool.corePoolSize}" />
    <!-- 线程池维护线程的最少数量 -->
    <property name="keepAliveSeconds" value="${threadpool.keepAliveSeconds}" />
    <!-- 线程池维护线程所允许的空闲时间 -->
    <property name="maxPoolSize" value="${threadpool.maxPoolSize}" />
    <!-- 线程池维护线程的最大数量 -->
    <property name="queueCapacity" value="${threadpool.queueCapacity}" />
    <!-- 线程池所使用的缓冲队列 -->
  </bean>

5. スレッドプールを使用する際の注意事項

•デッドロック

マルチスレッドプログラムにはデッドロックの危険性があります。最も単純なケースは、2つのスレッドAB、Aが保持されている状態です。 1 でロック 2 が要求されます。B はロック 2 を保持し、ロック 1 が要求されます。 (この状況は mysql の排他ロックでも発生し、データベースはエラー メッセージを直接報告します)。スレッド プールには別の種類のデッドロックがあります。スレッド プール内のすべてのワーカー スレッドがそれぞれのタスクの実行中にブロックされていると仮定すると、ワーカー スレッドは特定のタスク A の実行結果を待っています。ただし、タスク A はキュー内にあり、アイドル状態のスレッドがないため実行できません。このようにして、スレッド プール内のすべてのリソースが永久にブロックされ、デッドロックが発生します。

•システム リソースの不足

スレッド プール内のスレッドの数が非常に多い場合、これらのスレッドはメモリやその他のシステム リソースを含む大量のリソースを消費するため、システムのパフォーマンスに重大な影響を与えます。

•同時実行エラー

スレッド プールのワーク キューは wait() メソッドと Notice() メソッドに依存して、ワーカー スレッドが時間内にタスクを取得できるようにしますが、これら 2 つのメソッドは使用するのが困難です。コードが間違っていると、通知が失われ、ワーカー スレッドがアイドル状態のままになり、ワーク キュー内で処理する必要があるタスクが無視される可能性があります。より成熟したスレッド プールを使用することが最善であるためです。

•スレッド リーク

スレッド プールを使用する場合の重大なリスクは、スレッド リークです。ワーカー スレッドの数が固定されたスレッド プールの場合、タスクの実行時にワーカー スレッドが RuntimeException または Error をスローし、これらの例外またはエラーがキャッチされない場合、ワーカー スレッドは異常終了し、スレッド プールが永久に失われます。スレッド。 (これは非常に興味深いです)

もう 1 つの状況は、タスクの実行時にワーカー スレッドがブロックされ、ユーザーがデータを入力していない場合、スレッドはブロックされています。このようなワーカー スレッドは名前だけが存在し、実際にはタスクを実行しません。スレッド プール内のすべてのスレッドがこの状態にある場合、スレッド プールは新しいタスクを追加できません。

•タスクの過負荷

ワーカー スレッド キューに実行のためにキューに入れられたタスクが多数ある場合、タスク自体がシステム リソースを過剰に消費し、リソース枯渇を引き起こす可能性があります。

要約すると、スレッド プールを使用する場合は、次の原則に従う必要があります:

1. タスク A が実行中にタスク B の実行結果を同期的に待つ必要がある場合、タスク A はスレッド プールに追加するのには適していません。スレッドプールのワークキュー。タスク A のように、他のタスクの実行結果を待つ必要があるタスクをキューに追加すると、デッドロックが発生する可能性があります

2. タスクの実行中にブロックされる可能性があり、長時間ブロックされると、ワーカー スレッドが永続的にブロックされてスレッド リークが発生するのを防ぐために、 timeout を設定する必要があります。サーバー プログラムでは、スレッドがクライアントの接続を待機しているとき、またはクライアントから送信されるデータを待機しているときに、次の方法で時間を設定できます。

ServerSocket の setSotimeout メソッドを呼び出します。クライアントの接続を待機するタイムアウトを設定します。

クライアントに接続されているソケットごとに、ソケットの setSoTImeout メソッドを呼び出して、クライアントがデータを送信するのを待つタイムアウトを設定します。

3. タスクの特性を理解し、タスクが頻繁にブロックする IO 操作を実行するか、ブロックしない操作を実行するかを分析します。前者は断続的に CPU を占有しますが、後者は使用率が高くなります。短期タスクか長期タスクかにかかわらず、タスクの完了にかかる時間を見積もり、タスクの特性に従ってタスクを分類し、さまざまな種類のタスクを作業キューに追加します。タスクの特性に応じて処理できるように、異なるスレッドプールを割り当て、各スレッドプールの割り当てと調整を行います

4. スレッドプールのサイズを調整します。スレッド プールの最適なサイズは、主にシステム内で使用可能な CPU の数と、ワーク キュー内のタスクの特性によって決まります。 N 個の CPU を備えたシステム上にワーク キューが 1 つだけあり、それらがすべて計算 (非ブロッキング) タスクである場合、スレッド プールに N または N+1 個のワーカー スレッドがあると、通常、最大 CPU 使用率が得られます。レート。

IO 操作を実行し、頻繁にブロックするタスクがワーク キューに含まれている場合、すべてのワーカー スレッドが常に動作しているわけではないため、スレッド プールのサイズは使用可能な CPU の数を超える必要があります。典型的なタスクを選択し、このタスクを実行するプロジェクトで CPU が計算に費やした実際の時間に対する待機時間の比率 WT/ST を推定します。 N 個の CPU を備えたシステムの場合、CPU が完全に利用されるようにするには、約 N*(1+WT/ST) スレッドをセットアップする必要があります。

もちろん、スレッド プールを調整するときに考慮すべきことは CPU 使用率だけではありません。スレッド プール ジョブの数が増えると、メモリやその他のリソース (ソケット、開いているファイル ハンドル、データベースなど) にも制限が生じます。接続数などマルチスレッドによって消費されるシステムリソースが、システムが耐えられる範囲内であることを確認する必要があります。

5. タスクの過負荷を回避します。サーバーは、システムの収容能力に基づいて同時クライアント接続の数を制限する必要があります。クライアントの接続が制限を超えると、サーバーは接続を拒否してわかりやすいプロンプトを表示したり、キューの長さを制限したりできます。

上記は Java スレッド プールのいくつかの実装方法と FAQ の内容です。さらに関連する内容については、PHP 中国語 Web サイト (www.php.cn) をご覧ください。


声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。