[Java Sharing Inn] SpringBoot スレッド プールのパラメータは大量の情報を検索しても一致しません。この人生で理解できるように 1 日かけてテストしました。

I.はじめに

  まず、忙しくてふらっと立ち寄る場合はブックマークしておいて、時間のある時や利用するときに読んでみると良いと思いますが、以前の

  私と同じように混乱している人も多いと思います。スレッドプール. 非常に高さがあり、非常にファッショナブルに使用でき、ローカル環境のテスト環境でのデバッグには問題ありませんが、オンラインになるとすぐに問題が発生します。

  その後、Baidu は多くの情報を入手し、スレッド プールとさまざまな構成パラメータをカスタマイズする必要性について話していることがわかりました。

  最終的には、カスタム スレッド プールの構成パラメータを無視したことが原因であり、この記事では非常に単純なケースを使用して、スレッド プールの構成とオンライン環境の構成方法を明確にします。


第二に、ケース

1. ケースを書く

スレッド プールをカスタマイズし、初期構成を追加します。

コア スレッドの数は 10、スレッドの最大数は 50、キュー サイズは 200、カスタム スレッド プール名のプレフィックスは my-executor-、スレッド プール拒否ポリシーは AbortPolicy で、これもデフォルトです。タスクが直接放棄されることを示すポリシー。

package com.example.executor.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

@Configuration
@EnableAsync
@EnableScheduling
@Slf4j
public class AsyncConfiguration {
    
    

   /**
    * 自定义线程池
    */
   @Bean(name = "myExecutor")
   public Executor getNetHospitalMsgAsyncExecutor() {
    
    
      log.info("Creating myExecutor Async Task Executor");
      ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
      executor.setCorePoolSize(10);
      executor.setMaxPoolSize(50);
      executor.setQueueCapacity(200);
      executor.setThreadNamePrefix("my-executor-");
      // 拒绝策略:直接拒绝抛出异常
      executor.setRejectedExecutionHandler(
            new ThreadPoolExecutor.AbortPolicy());
      return executor;
   }
}

次に、非同期サービスを作成し、このカスタム スレッド プールを直接使用して、5 秒かかるメッセージ送信サービスをシミュレートします。

package com.example.executor.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 * 异步服务
 * </p>
 *
 * @author 福隆苑居士,公众号:【Java分享客栈】
 * @since 2022/4/30 11:41
 */
@Service
@Slf4j
public class AsyncService {
    
    

   /**
    * 模拟耗时的发消息业务
    */
   @Async("myExecutor")
   public void sendMsg() throws InterruptedException {
    
    
      log.info("[AsyncService][sendMsg]>>>> 发消息....");
      TimeUnit.SECONDS.sleep(5);
   }
}

次に、TestService を作成し、Hutools に付属の同時実行ツールを使用して上記のメッセージング サービスを呼び出し、同時実行数を 200 に設定します。つまり、同時に 200 個のスレッドを開いてビジネスを実行します。

package com.example.executor.service;

import cn.hutool.core.thread.ConcurrencyTester;
import cn.hutool.core.thread.ThreadUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
 * <p>
 * 测试服务
 * </p>
 *
 * @author 福隆苑居士,公众号:【Java分享客栈】
 * @since 2022/4/30 11:45
 */
@Service
@Slf4j
public class TestService {
    
    

   private final AsyncService asyncService;

   public TestService(AsyncService asyncService) {
    
    
      this.asyncService = asyncService;
   }

   /**
    * 模拟并发
    */
   public void test() {
    
    
      ConcurrencyTester tester = ThreadUtil.concurrencyTest(200, () -> {
    
    
         // 测试的逻辑内容
         try {
    
    
            asyncService.sendMsg();
         } catch (InterruptedException e) {
    
    
            log.error("[TestService][test]>>>> 发生异常: ", e);
         }
      });

      // 获取总的执行时间,单位毫秒
      log.info("总耗时:{}", tester.getInterval() + " ms");
   }
}

最後に、テスト インターフェイスを作成します。

package com.example.executor.controller;

import com.example.executor.service.TestService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * <p>
 * 测试接口
 * </p>
 *
 * @author 福隆苑居士,公众号:【Java分享客栈】
 * @since 2022/4/30 11:43
 */
@RestController
@RequestMapping("/api")
public class TestController {
    
    

   private final TestService testService;

   public TestController(TestService testService) {
    
    
      this.testService = testService;
   }

   @GetMapping("/test")
   public ResponseEntity<Void> test() {
    
    
      testService.test();
      return ResponseEntity.ok().build();
   }
}

2. 実行順序

ケースが終了したら、スレッド プールを呼び出すテストを開始しますが、その前に、実行プロセス中にカスタム スレッド プールの設定がどのように実行され、どのような順序で実行されるかを説明します。クリア、後でパラメータを調整するときに混乱することはありません。

コアスレッドの数 (CorePoolSize) —> (すべてが占有されている場合) —> キューに入れられます (QueueCapacity) —> (すべてが占有されている場合) —> スレッドの最大数 (MaxPoolSize) に従って新しいスレッドを作成します —> (スレッドの最大数を超えた場合) —> 拒否ポリシー (RejectedExecutionHandler) の実行を開始します。

3回続けて見れば、そうなります。


3. コアスレッド数の設定方法

まずプログラムを実行してみましょう。ここで、全員が聞けるように、上記の事件の重要な手がかりをもう一度説明します。

1) スレッド プールのコア スレッド数は 10、スレッドの最大数は 50、キューは 200、2) メッセージの

送信には 5 秒かかります、3)

ツールの同時実行スレッド数は 200 です。

下の図でわかるように、200 個のスレッドがすべて実行されました。左側の時間を見ると、5 秒ごとに 10 個のスレッドが実行されます。200 個のスレッドすべてを実行すると非常に遅いことがはっきりとわかります。バックグラウンド操作。

10 個のコア スレッドが最初に実行され、残りの 190 個がキューに配置され、キュー サイズが 200 であるため、スレッドの最大数は機能しません。

111.png

考える: 200 スレッドの実行効率を向上させるにはどうすればよいでしょうか? 私たちのビジネスは 5 秒かかる時間のかかるビジネスであるため、答えはすでに明らかですが、コア スレッドの数を少なく設定すると、200 スレッドすべての実行が非常に遅くなるため、コア スレッドの数を増やすだけで済みます。コアスレッドです。

コアスレッドの数を100に調整します

@Bean(name = "myExecutor")
   public Executor getNetHospitalMsgAsyncExecutor() {
    
    
      log.info("Creating myExecutor Async Task Executor");
      ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
      executor.setCorePoolSize(100);
      executor.setMaxPoolSize(50);
      executor.setQueueCapacity(200);
      executor.setThreadNamePrefix("my-executor-");
      // 拒绝策略:直接拒绝抛出异常
      executor.setRejectedExecutionHandler(
            new ThreadPoolExecutor.AbortPolicy());
      // 拒绝策略:调用者线程执行
//    executor.setRejectedExecutionHandler(
//          new ThreadPoolExecutor.CallerRunsPolicy());
      return executor;
   }

効果を見てください。え?エラーを報告しましたか?

222.png

ソースコードを見てください。

333.png

スレッドプールの初期化時に内部判定が行われることが分かり、最大スレッド数がコアスレッド数よりも少ない場合にこの例外がスローされるため、少なくともスレッド数を設定する際には特に注意する必要があります。コア スレッドの数は、スレッドの最大数以上である必要があります。

構成を変更しましょう。コア スレッドの数と最大スレッド数は両方とも 100 に設定されます。

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(100);
executor.setMaxPoolSize(100);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("my-executor-");

効果を見てください: バックグラウンド実行プロセス中、実行速度が非常に速く、以前より少なくとも 10 倍速く、しばらくすると 200 スレッドの実行が終了することがわかります。

444.png

理由: 時間のかかる業務を 5 秒に設定し、コア スレッドの数は 10 しかありません。そうすると、キューで待機しているスレッドが時間のかかる業務をバッチで実行するため、5 秒ごとのバッチが非常に遅くなります。 . コアを入れると スレッド数が増えると、5秒の業務を1~2バッチ実行するだけで済むことになり、当然速度は2倍になります。

ここで最初の結論を導き出すことができます。

ビジネスに時間がかかる場合は、スレッド プール構成内のコア スレッドの数を増やす必要があります。

しばらく考えてください:

より少ない数のコアスレッドとより大きなキューを構成するには、どのようなビジネスが適していますか?


4. 最大スレッド数の設定方法

次に、スレッドの最大数を見てみましょう。これは興味深いことです。インターネット上の多くの情報は間違っています。

前のケースのままですが、わかりやすくするために、構成パラメーターを調整しましょう。コア スレッドの数は 4、スレッドの最大数は 8、キューは 1 つだけです。

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(1);
executor.setThreadNamePrefix("my-executor-");

次に、同時テストの数を 10 に変更しました。

ConcurrencyTester tester = ThreadUtil.concurrencyTest(10, () -> {
    
    
   // 测试的逻辑内容
   try {
    
    
      asyncService.sendMsg();
   } catch (InterruptedException e) {
    
    
      log.error("[TestService][test]>>>> 发生异常: ", e);
   }
});

開始、テスト:

驚きの発見ですね?同時実行数は 10 ですが、なぜ 9 つだけが実行され、残りの 1 つはどこへ行くのでしょうか?

555.png
最大スレッド数を 7 に変更して再試行してみましょう

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(7);
executor.setQueueCapacity(1);
executor.setThreadNamePrefix("my-executor-");

もう一度見てみると、処刑されたのは8人だけで、終わってみればそのうちの2人は行方不明になっていました…。

666.png

具体的なデモンストレーション効果については、次の拒否戦略で実証しますが、ここで結論を直接お伝えします。

  スレッド プール内のスレッドの最大数は何を意味しますか? はい、文字通りの意味です。コアスレッドの数がいっぱいになるとキューもいっぱいになり、残りのスレッドは最大スレッド数で作成された新しいスレッドを通じてタスクを実行するという、このプロセスは最初から誰にでも整理されています。

  ただし、注意してください。これはスレッドの最大数であるため、実行スレッドはこの数を超えることはなく、この数を超えると拒否ポリシーによって拒否されます。

  このセクションの冒頭の設定パラメータに基づいてもう一度整理してみましょう。同時実行数は 10、コア スレッドは 4 つ占有、キューに入るスレッドは 1 つ、スレッドの最大数は 8 に設定されています。現在の 2 秒では、ビジネスタイム、アクティブなスレッドの総数は次のとおりです:

  コアスレッドの数 (4) + 新しく作成されたスレッドの数 (?) = 最大スレッド数 (8) スレッドの最大数が次のように

  構成されていることがわかります。 8、コア スレッドとキューの数がいっぱいになった後、新しく作成されるスレッドの数は 8-4=4 のみであるため、最終的な実行は次のようになります: コア スレッドの数 (4) + 新しく作成されるスレッドの

  数(4) + キュー内のスレッド数 (1) = 9

  まったく問題はなく、残りの 1 つが最大スレッド数 8 を超えたため、拒否ポリシーによって拒否されました。

最後に、図で明確に説明します。左側の時間に注目すると、最後のスレッドがキュー内のスレッドで、2 秒後に実行されることがわかります。

777.png

ここで、2 番目の結論も引き出す​​ことができます。

スレッドの最大数は文字通りの意味です。現在アクティブなスレッドはこの制限を超えることはできません。この制限を超えると、拒否ポリシーによって拒否されます。


5. キューサイズの設定方法

最初の 2 つは理解されており、キューのサイズは実際に簡単なテストで理解できます。
以前のスレッド プール構成を変更します。

コア スレッドの数は 50、スレッドの最大数は 50、キューは 100、テストを容易にするためにビジネス タイムは 1 秒に変更されます。

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(50);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("my-executor-");

同時実行数は 200 に設定されています

ConcurrencyTester tester = ThreadUtil.concurrencyTest(200, () -> {
    
    
   // 测试的逻辑内容
   try {
    
    
      asyncService.sendMsg();
   } catch (InterruptedException e) {
    
    
      log.error("[TestService][test]>>>> 发生异常: ", e);
   }
});

効果をテストします: 200 個の同時実行数のうち、最終的には 150 個だけが実行されることがわかります。特定のアルゴリズムにおけるスレッドの最大数については、前のセクションで説明したので繰り返しません。

888.png

ここでは主に、現在のスレッド数がキュー サイズを超えた後、ビジネスを実行するための新しいスレッドの計算と作成に最大スレッド数が使用されることを明確にします。キューが再び最大数のスレッドを消費しないように、キューを大きくする必要があります。

キューのサイズを 100 から 500 まで調整して確認してみましょう。

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(50);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("my-executor-");

テスト結果: ご覧のとおり、200 件すべてが実行されました。これは、私たちの仮定が正しいことを示しています。

999.png

ここで 3 番目の結論を導き出すことができます。

キュー サイズが適切に設定されている場合は、最大スレッド数を使用して追加のオーバーヘッドを発生させる必要がないため、スレッド プールを構成する最良の方法は、コア スレッドの数とキュー サイズを一致させることです。


6. 拒否戦略を一致させる方法

スレッドの最大数を構成する方法に関する前のセクションでは、最初に拒否ポリシーを構成したため、テスト後、スレッドの最大数を超えた後に一部のスレッドが直接拒否されることがわかります。スレッド プールのデフォルト ポリシー。これは直接拒否を意味します。

// 拒绝策略:直接拒绝抛出异常
executor.setRejectedExecutionHandler(
      new ThreadPoolExecutor.AbortPolicy());

では、これらのスレッドが実際に拒否されたことをどのようにして知ることができるのでしょうか。ここでは、スレッドの最大数セクションのパラメータ設定を復元します。

次に、デフォルトのポリシーを別のポリシー CallerRunsPolicy に変更します。これは、呼び出し側スレッドが拒否後も実行を継続することを意味します。

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(7);
executor.setQueueCapacity(1);
executor.setThreadNamePrefix("my-executor-");
// 拒绝策略:调用者线程执行
executor.setRejectedExecutionHandler(
      new ThreadPoolExecutor.CallerRunsPolicy());
return executor;

同時実行数が 10 に変更されます

ConcurrencyTester tester = ThreadUtil.concurrencyTest(10, () -> {
    
    
   // 测试的逻辑内容
   try {
    
    
      asyncService.sendMsg();
   } catch (InterruptedException e) {
    
    
      log.error("[TestService][test]>>>> 发生异常: ", e);
   }
});

試験結果:

  10 個の同時実行数がすべて実行されており、スレッドの最大数に関するセクションでは、テスト時にデフォルトの戦略によって 2 つのスレッドが拒否されました。これは、呼び出し元のスレッドが引き続き実行できるように戦略が変更されたためです。タスクを実行すると、2 つのスレッドが拒否されても、呼び出し元のスレッドによって実行されます。

  図の赤線で囲まれた 2 つのスレッドの名前は、実行する呼び出し側スレッドであるカスタム スレッドの名前とは明らかに異なっていることがわかります。

1010.png

  では、この戦略は非常に人道的で、良いものに違いないのでしょうか?

  いいえ!この戦略は制御できません。インターネット プロジェクトの場合、オンラインでは失敗しやすいのです。理由は非常に簡単です。

  スレッド プールが占めるのはメイン スレッドではなく、タスクを実行するための非同期操作です。この戦略は、実際には、拒否されたスレッドを実行のためにメイン スレッドに再度渡します。これは、非同期を同期に変更するのと同じです。想像してみてください。ピーク トラフィックではこの戦略により、多数の非同期スレッドがメイン スレッドから離れると、その結果はどうなるでしょうか? メイン スレッドのプログラムがクラッシュし、サービス雪崩が発生する可能性があります。

スレッド プールによって提供される 4 つの戦略を示します。

1)、AbortPolicy: 直接拒否して RejectedExecutionException をスローするデフォルト ポリシー、

2)、CallerRunsPolicy: 呼び出し側スレッドがタスクの実行を継続する、単純なフィードバック メカニズム ポリシー、

3)、DiscardPolicy: 通知なしでタスクを直接破棄し、フィードバック;

4)、DiscardOldestPolicy: 古いタスク (通常は生存時間が最も長いタスク) を破棄します。

CallerRunsPolicy 戦略が最も完全であると多くの人が考えていますが、私の個人的な意見では、実稼働環境ではデフォルトの戦略が実際に最もリスクが低く、オンライン プロジェクトではセキュリティを優先する傾向があります。


そういえば、この例からすると、このスレッドプールのパラメータの意味は基本的に誰でも理解できるので、以前出した思考の質問をまだ覚えていますか? 覚えていないのは、みんな魚だからです。は:

より少ない数のコアスレッドとより大きなキューを構成するには、どのようなビジネスが適していますか?

  回答: 低消費時間と高同時実行性のシナリオは、非常に適しています。低時間消費はミリ秒レベルのビジネスに属し、CPU とメモリにより適しているからです。高同時実行性が高い場合はキュー バッファリングが必要であり、消費時間が少ないため、キュー バッファリングが必要です。キューで長時間待機することはありません。コア スレッドの数が多いと一度に CPU オーバーヘッドが増加するため、コア スレッドの数を減らしてキューを大きくする構成は、このシナリオに非常に適しています。

  余談ですが、クラウド製品を使用したことがある方は、クラウド サーバーを購入するときに、CPU 集中型モデルか IO 集中型モデルを選択するよう常に求められることをご存じかと思います。スレッド プールについて詳しく知っていれば、それが何を意味するかがわかるでしょう。 , さまざまなプロジェクトに適合させる必要があるサーバー モデルが実際に考慮されます。上記のシナリオでは、CPU 集中型のサーバーを選択するのは明らかです。ただし、前章のケース シナリオは時間がかかり、IO に適しています。集中的なサーバー。


3. まとめ

この章の要約に加えて、私の実務経験から得た追加のポイントがあります。

1) 時間のかかるビジネスの場合は、スレッド プール構成のコア スレッドの数を増やし、キューを適切に減らす必要があります。2) 時間のかかるビジネスの場合は (ミリ秒レベル

) )、同時にトラフィックが大きい場合は、スレッド プール構成のコア スレッドの数を減らし、キューを適切に増やす必要があります。3)、スレッドの最大数は文字通りの意味で、現在アクティブなスレッドの数です

。スレッドはこの制限を超えることはできません。この制限を超えると、ポリシーによって拒否されます。拒否します。4

) キュー サイズが適切に設定されている場合、追加のオーバーヘッドを引き起こすためにスレッドの最大数を使用する必要はありません。スレッド プールを構成する最良の方法は、コア スレッドの数とキュー サイズを一致させることです。5)、スレッド プールの拒否戦略はできる限りデフォルトに基づいて、運用環境のリスクを軽減し、実行しないでください

。必要な場合を除いて変更する;

6)、同じサーバーにデプロイされるプロジェクトまたはマイクロサービスのスレッド プールの総数は 5 を超えてはなりません、そうでない場合、生死は生命と富に依存します; 7)、スレッド プールを無差別に使用しないでください

。ビジネス シナリオを明確にし、ここでメッセージを送信する、サブスクリプション通知を送信する、または特定のシナリオのログ記録を実行するなど、遅延する可能性があり、特に重要ではないシナリオでそれらを使用するようにしてください。スレッドを使用するのは簡単です。コア ビジネスのプール;

8)、スレッド プールを混合しないでください、特定のビジネスを分離することを忘れないでください、つまり、独自のスレッド プール、異なる名前、異なるパラメータをカスタマイズすることを忘れないでください。スレッド プールを自由に作成したと想像できます。自分のビジネスに適したパラメータを見つけましたが、大量の同時実行を行うビジネスの別の同僚によって使用されていました。そのとき、私は同じ問題を抱えているだけです。9)、スレッド プールの構成は夕食のおやつではありません

。 、たとえ慣れていても、オンラインにする前にストレス テストを行ってください。これは私にとって痛い教訓です。10

)、スレッド プールのアプリケーション シナリオを必ず明確にして、スレッド プールのアプリケーション シナリオを混同しないでください。同時実行性の高い処理ソリューションでは、2 つのビジネスの方向性はまったく異なります。


4. 共有

  最後に、私が以前の仕事で使用した公式を共有します。これは、中小企業の特定のビジネスの現在のスレッドが数千レベルを超えるシナリオのみを対象としています。大規模な工場で働いたことがないので、共有できる経験は限られています。

  当社を例に挙げます。当社は中小規模のインターネット会社です。Huawei Cloud を使用しており、オンライン サーバーは基本的に 8 コアです。通常、特定の企業向けのスレッド プールを使用して、現在のスレッド数でテストします。 2000 (2000 個のスレッドが同時に実行されているため) 中小企業では、並列スレッドを実現するのは、誰もが思っているほど簡単ではありません。当社は病院にサービスを提供していますが、過去2年間の流行による核酸の数の急増を除けば、病院に会うのは年に数回です。

  ある業務を 2,000 スレッドで同時に処理しているときに、どのくらいのユーザーが必要で、どのような場面になるのかを想像してみてください。重要なのは、スレッド プールを使用することです。なぜスレッド プール自体を使用するのかということです。このシーンでは? これも反省すべき点です。同様のシナリオの中には、ピークをカットするためにキャッシュと MQ を使用しているものもあります。まとめで同時実行性の高い処理ソリューションと混同しないようにと述べたのはこのためです。処理を遅延させる場合はスレッド プールを使用する必要があります。重要度の低い業務に最適です。

私がまとめた式はここから入手できます:
リンク: https://pan.baidu.com/doc/share/TES95Wnsy3ztUp_Al1L~LQ-567189327526315
抽出コード: 2jjy



私のオリジナルの記事は純粋に手書きです。役に立ったと思われる場合は、高評価をお願いします

実際の仕事での経験や面白かったことなどを随時シェアしていきますので、興味のある方はぜひご覧ください〜


おすすめ

転載: blog.csdn.net/xiangyangsanren/article/details/124532681