12 分で Executor からスレッド プールを上から下まで完全に理解する

序文

前回の記事では、並行パッケージで一般的に使用される同期コンポーネントと手書きのカスタム同期コンポーネントについて説明するのに 13 分かかりました。並行パッケージで一般的に使用される同期コンポーネントについて説明し、カスタム同期コンポーネントも段階的に実装しました。

この記事では、同時実行パッケージの別のコアであるスレッド プールについて説明します。

この記事を読むのに必要な時間は約 12 分です

この記事を読む前に、スレッド プールについて理解しているかどうかを確認するために、いくつかの質問を見てみましょう。

  1. プーリング技術とは何ですか? どのような機能があり、どのようなシナリオで使用されますか?
  2. 執行者とは何ですか?その設計哲学とは何ですか?
  3. 作業の種類は何種類ありますか? 特徴は何ですか? どのように適応させて執行者に引き渡すか?
  4. スレッドプールはどのように実装されますか? 主要なパラメータとその設定方法は何ですか? ワークフローとは何ですか?
  5. スレッド プールは例外をどのように適切に処理するのでしょうか? スレッドプールをオフにするにはどうすればよいですか?
  6. タイミングを処理するスレッド プールはどのように実装されていますか?

プーリング技術

スレッドの作成と破棄には一定のオーバーヘッドがかかります

必要なときに複数のスレッドを作成し、使用後に破棄すると、ビジネス プロセスが長くなるだけでなく、スレッドの作成と破棄にかかるオーバーヘッドも増加します。

そこで思いついたのが、スレッドを事前に作成し、プール(コンテナ)で管理するプーリング技術です。

必要に応じて、プールからスレッドを取得してタスクを実行し、実行完了後にスレッドをプールに戻します。

スレッドだけでなく、接続にもプーリングという概念があります。つまり、接続プーリングです。

プーリング技術はリソースを再利用してレスポンスを向上させるだけでなく、管理も容易にします。

エグゼキューターフレームワーク

Executor フレームワークとは何ですか?

エグゼキュータは、タスクの実行方法を定義するスレッド プールの抽象化として一時的にみなすことができます。

  public interface Executor {
      void execute(Runnable command);
  }

Executor作業タスクをスレッド プールから分離および切り離す

画像.png

作業タスクは、結果を返さないタスクRunnableと結果を返すタスクの 2 つのタイプに分類されます。Callable

どちらのタスクもスレッド プールで許可されます。どちらも関数インターフェイスであり、ラムダ式を使用して実装できます。

学生の中には疑問を持つ人もいるかもしれませんが、上記のExecutorフレームワークで定義された実行方法では、受信したRunnableタスクのみが許可されるのではありませんか?

Callableタスクはどのメソッドを呼び出して実行しますか?

Futureこのインターフェースは、非同期タスクの結果の取得を定義するために使用されます。その実装クラスは、多くの場合、FutureTask

FutureTask実装と同時にRunnableフィールドストレージも使用しCallable実装時に実際にタスクを実行します。RunnableCallable

スレッド プールがCallableタスクを実行すると、そのタスクFutureTaskがカプセル化されてRunnable実行されるため (具体的なソース コードについては後で説明します)、Executor実行メソッドには次のようなものしかありません。Runnable

FutureTaskアダプターに相当し、Callable変換されてRunnable実行されます。

画像.png

Executor はスレッド プールを定義します。その重要な実装は次のとおりです。ThreadPoolExecutor

これに基づいてThreadPoolExecutor、タイミング用のスレッド プールもありますScheduledThreadPoolExecutor

画像.png

スレッドプールエグゼキュータ

コアパラメータ

ThreadPoolExecutor7 つの主要な重要なパラメータがあります

  public ThreadPoolExecutor(int corePoolSize,
                                int maximumPoolSize,
                                long keepAliveTime,
                                TimeUnit unit,
                                BlockingQueue<Runnable> workQueue,
                                ThreadFactory threadFactory,
                                RejectedExecutionHandler handler)
  1. corePoolSize スレッド プール内のコア スレッドの数
  2. minimumPoolSize スレッド プールによって作成できるスレッドの最大数
  3. keepAliveTime タイムアウト、TimeUnit 時間単位: 非コアスレッドがアイドル状態になった後に存続する時間
  4. workQueue は、タスクの実行を待機しているブロッキング キューを格納します。
  5. threadFactory スレッド ファクトリ: スレッドの作成方法を指定し、さまざまなビジネスに応じてさまざまなスレッド グループ名を指定できます。
  6. RejectedExecutionHandler の拒否戦略: 十分なスレッドがなく、ブロッキング キューがいっぱいの場合にタスクを拒否する方法
拒否ポリシー 効果
中止ポリシーのデフォルト 例外をスローする
CallerRunsポリシー スレッドを呼び出してタスクを実行する
ポリシーの破棄 加工しないで廃棄する
最も古いポリシーを破棄 キュー内の最新のタスクを破棄し、現在のタスクをすぐに実行します

構築中のコア パラメータに加えて、スレッド プールは内部クラスを使用してWorkerスレッドとタスクをカプセル化し、HashSet コンテナworkes作業キューを使用してワーカー スレッドを格納します。

実施原則

フローチャート

スレッド プールの実装原理を明確に理解するために、最初にフローチャートと概要で原理の概要を説明し、最後にソース コードの実装を見ていきます。

画像.png

  1. ワーカースレッドの数がコアスレッドの数より少ない場合は、スレッドを作成し、ワークキューに参加し、タスクを実行します
  2. ワーカー スレッドの数がコア スレッドの数以上で、スレッド プールがまだ実行中の場合は、タスクをブロッキング キューに追加してみてください。
  3. タスクがブロッキング キューに参加できず (ブロッキング キューがいっぱいであることを示します)、ワーカー スレッドの数がスレッドの最大数より少ない場合は、実行用のスレッドを作成します。
  4. ブロッキング キューがいっぱいで、ワーカー スレッドの数が最大スレッド数に達すると、拒否ポリシーが実行されます。
実行する

スレッド プールには、execute と submit の 2 つの送信メソッドがあります。Submit は RunnableFuture にカプセル化され、最終的に実行されます。execute

      public <T> Future<T> submit(Callable<T> task) {
          if (task == null) throw new NullPointerException();
          RunnableFuture<T> ftask = newTaskFor(task);
          execute(ftask);
          return ftask;
      }

executeスレッドプールの実行プロセス全体を実装します。

  public void execute(Runnable command) {
      //任务为空直接抛出空指针异常
      if (command == null)
          throw new NullPointerException();
      //ctl是一个整型原子状态,包含workerCount工作线程数量 和 runState是否运行两个状态
      int c = ctl.get();
      //1.如果工作线程数 小于 核心线程数 addWorker创建工作线程
      if (workerCountOf(c) < corePoolSize) {
          if (addWorker(command, true))
              return;
          c = ctl.get();
      }
      
      // 2.工作线程数 大于等于 核心线程数时
      // 如果 正在运行 尝试将 任务加入队列
      if (isRunning(c) && workQueue.offer(command)) {
          //任务加入队列成功 检查是否运行
          int recheck = ctl.get();
          //不在运行 并且 删除任务成功 执行拒绝策略 否则查看工作线程为0就创建线程
          if (! isRunning(recheck) && remove(command))
              reject(command);
          else if (workerCountOf(recheck) == 0)
              addWorker(null, false);
      }
      // 3.任务加入队列失败,尝试去创建非核心线程,成功则结束
      else if (!addWorker(command, false))
          // 4.失败则执行拒绝策略
          reject(command);
  }
追加ワーカー

addWorkerワークキューに参加してタスクを実行するためのスレッドを作成するために使用されます。

2 番目のパラメーターは、コア スレッドを作成するかどうかを決定するために使用され、コア スレッドが作成される場合は true、非コア スレッドが作成される場合は false となります。

  private boolean addWorker(Runnable firstTask, boolean core) {
          //方便跳出双层循环
          retry:
          for (;;) {
              int c = ctl.get();
              int rs = runStateOf(c);
  
              // Check if queue empty only if necessary.
              // 检查状态
              if (rs >= SHUTDOWN &&
                  ! (rs == SHUTDOWN &&
                     firstTask == null &&
                     ! workQueue.isEmpty()))
                  return false;
  
              for (;;) {
                  int wc = workerCountOf(c);
                  //工作线程数已满 返回false 
                  if (wc >= CAPACITY ||
                      wc >= (core ? corePoolSize : maximumPoolSize))
                      return false;
                  //CAS自增工作线程数量 成功跳出双重循环
                  if (compareAndIncrementWorkerCount(c))
                      break retry;
                  //CAS失败 重新读取状态 内循环
                  c = ctl.get();  // Re-read ctl
                  if (runStateOf(c) != rs)
                      continue retry;
                  // else CAS failed due to workerCount change; retry inner loop
              }
          }
  
          //来到这里说明已经自增工作线程数量 准备创建线程
          boolean workerStarted = false;
          boolean workerAdded = false;
          Worker w = null;
          try {
              //创建worker 通过线程工厂创建线程
              w = new Worker(firstTask);
              final Thread t = w.thread;
              if (t != null) {
                  //全局锁
                  final ReentrantLock mainLock = this.mainLock;
                  mainLock.lock();
                  try {
                      // Recheck while holding lock.
                      // Back out on ThreadFactory failure or if
                      // shut down before lock acquired.
                      int rs = runStateOf(ctl.get());
  
                      if (rs < SHUTDOWN ||
                          (rs == SHUTDOWN && firstTask == null)) {
                          if (t.isAlive()) // precheck that t is startable
                              throw new IllegalThreadStateException();
                          //添加线程
                          workers.add(w);
                          int s = workers.size();
                          if (s > largestPoolSize)
                              largestPoolSize = s;
                          //标记线程添加完
                          workerAdded = true;
                      }
                  } finally {
                      mainLock.unlock();
                  }
                  //执行线程
                  if (workerAdded) {
                      t.start();
                      workerStarted = true;
                  }
              }
          } finally {
              if (! workerStarted)
                  addWorkerFailed(w);
          }
          return workerStarted;
      }

addWorkerZhonghui CAS は、作業スレッドの数を自動的に増やし、スレッドを作成してロックし、スレッドを作業キュー (ハッシュセット) に追加し、ロックを解除した後、スレッドを開始してタスクを実行します。

ランワーカー

ワーカーに実装されるのはメソッドRunnableであり、スレッドを開始した後はタスクの実行を継続し、タスクの実行後にタスクの実行を取得します。runWorker

  final void runWorker(Worker w) {
      Thread wt = Thread.currentThread();
      Runnable task = w.firstTask;
      w.firstTask = null;
      w.unlock(); // allow interrupts
      boolean completedAbruptly = true;
      try {
          //循环执行任务 getTask获取任务
          while (task != null || (task = getTask获取任务()) != null) {
              w.lock();
              // If pool is stopping, ensure thread is interrupted;
              // if not, ensure thread is not interrupted.  This
              // requires a recheck in second case to deal with
              // shutdownNow race while clearing interrupt
              if ((runStateAtLeast(ctl.get(), STOP) ||
                   (Thread.interrupted() &&
                    runStateAtLeast(ctl.get(), STOP))) &&
                  !wt.isInterrupted())
                  wt.interrupt();
              try {
                  //执行前 钩子方法
                  beforeExecute(wt, task);
                  Throwable thrown = null;
                  try {
                      //执行
                      task.run();
                  } catch (RuntimeException x) {
                      thrown = x; throw x;
                  } catch (Error x) {
                      thrown = x; throw x;
                  } catch (Throwable x) {
                      thrown = x; throw new Error(x);
                  } finally {
                      //执行后钩子方法
                      afterExecute(task, thrown);
                  }
              } finally {
                  task = null;
                  w.completedTasks++;
                  w.unlock();
              }
          }
          completedAbruptly = false;
      } finally {
          processWorkerExit(w, completedAbruptly);
      }
  }

実行の前後に 2 つの空のフック メソッドを予約し、サブクラスが展開できるように残しておきます。これらは、後でスレッド プールの例外を処理するためにも使用されます。

設定パラメータ

スレッド プール内のスレッドが多いほど良いのでしょうか?

まず、スレッドの作成にはオーバーヘッドがあることを理解する必要があり、プログラム カウンター、仮想マシン スタック、およびローカル メソッド スタックはすべてスレッドのプライベート空間です。

そして、スレッドがスペースを申請すると、CAS を通じて若い世代の Eden 領域のメモリを申請します (複数のスレッドが同時に申請する可能性があるため、CAS が必要です)

スレッドが多すぎると、使用される Eden スペースが多すぎて gc が若くなる可能性があり、スレッド コンテキストの切り替えにもオーバーヘッドが必要になります。

したがって、スレッド プール内のスレッドが多いほど良いことになり、業界では 2 つの一般的なソリューションに分かれています。

CPU を集中的に使用するアプリケーションの場合、スレッド プールはスレッドの最大数を CPU コアの数 + 1 に設定して、コンテキストの切り替えを回避し、スループットを向上させ、収益を保護するためにもう 1 つのスレッドを残します。

IO が集中する状況では、スレッド プールはスレッドの最大数を CPU コア数の 2 倍に設定します。IO は待機する必要があるため、CPU のアイドル状態を避けるためにより多くのスレッドが必要になります。

特定のビジネス シナリオでは詳細な分析が必要であり、最も合理的な構成を取得するために多数のテストが追加されます。

Executor フレームワークは、次のような静的ファクトリ メソッドを通じていくつかのスレッド プールを提供しExecutors.newSingleThreadExecutor()ますExecutors.newFixedThreadPool()Executors.newCachedThreadPool()

ただし、ビジネス シナリオが異なるため、スレッド プールをカスタマイズすることが最善であり、スレッド プールのパラメータと実装原則を理解した後、ソース コードを表示するのは難しくありません。

例外を処理する

スレッド プールで例外が発生するとどうなりますか?

実行可能

タスクを使用するとRunnable、例外が直接スローされます

         threadPool.execute(() -> {
             int i = 1;
             int j = 0;
             System.out.println(i / j);
         });

この状況に直面して、Runnable タスクで try-catch を使用してキャプチャできます。

         threadPool.execute(() -> {
             try {
                 int i = 1;
                 int j = 0;
                 System.out.println(i / j);
             } catch (Exception e) {
                 System.out.println(e);
             }
         });

実際の操作では、コンソールに出力する代わりにログを使用します。

呼び出し可能

タスクを使用する場合Callable、submit メソッドを使用すると、Future

         Future<Integer> future = threadPool.submit(() -> {
             int i = 1;
             int j = 0;
             return i / j;
         });

戻り値を取得するために使用しない場合Future.get()、例外はスローされないため、より危険です。

なぜそのような状況が起こるのでしょうか?

前述したように、submit を実行すると、実行Callableにカプセル化されます。FutureTask

Runnable の実装では、Callable タスクを実行するときに例外が発生すると、それは FutureTask にカプセル化されます。

     public void run() {
         //...其他略
         try {
             //执行call任务
             result = c.call();
             ran = true;
         } catch (Throwable ex) {
             //出现异常 封装到FutureTask
             result = null;
             ran = false;
             setException(ex);
         }
         //..
     }

get を実行すると、まずブロックしてタスクが完了するまでステータスを判定し、ステータスが異常であればカプセル化された例外をスローします。

     private V report(int s) throws ExecutionException {
         Object x = outcome;
         if (s == NORMAL)
             return (V)x;
         if (s >= CANCELLED)
             throw new CancellationException();
         throw new ExecutionException((Throwable)x);
     }

したがって、Callableタスクを処理するときに、タスクをキャプチャまたは取得できます。

         //捕获任务
         Future<?> f = threadPool.submit(() -> {
             try {
                 int i = 1;
                 int j = 0;
                 return i / j;
             } catch (Exception e) {
                 System.out.println(e);
             } finally {
                 return null;
             }
         });
 ​
         //捕获get
         Future<Integer> future = threadPool.submit(() -> {
             int i = 1;
             int j = 0;
             return i / j;
         });
 ​
         try {
             Integer integer = future.get();
         } catch (Exception e) {
             System.out.println(e);
         }
実行後

スレッドプールを覚えていますrunWorkerか?

ブロッキングキュー内のタスクの実行をループ内で継続的に取得し、実行前後にフックメソッドを予約します。

実行後にフックメソッドを書き換えるために継承しThreadPoolExecutor、実行後に例外が発生したかどうかを記録し、例外が発生した場合はそれを記録して隠蔽計画のレイヤーを作成します

 public class MyThreadPool extends ThreadPoolExecutor {  
     //...
     
     @Override
     protected void afterExecute(Runnable r, Throwable t) {
         //Throwable为空 可能是submit提交 如果runnable为future 则捕获get
         if (Objects.isNull(t) && r instanceof Future<?>) {
             try {
                 Object res = ((Future<?>) r).get();
             } catch (InterruptedException e) {
                 Thread.currentThread().interrupt();
             } catch (ExecutionException e) {
                 t = e;
             }
         }
 ​
         if (Objects.nonNull(t)) {
             System.out.println(Thread.currentThread().getName() + ": " + t.toString());
         }
     }
 }

このようにすると、submit を使用して get を使用するのを忘れた場合でも、例外は「消える」ことはありません。

setUncaughtException

スレッドを作成するときに、uncaughtExceptionスレッドでキャッチされない例外が発生したときに呼び出される、キャッチされない例外メソッドを設定できます。また、完全な情報についてログを出力することもできます。

独自のスレッド ファクトリを定義し、ビジネス グループ グループに基づいてスレッドを作成し (エラーのトラブルシューティングを容易にするため)、uncaughtExceptionメソッドを設定します。

 public class MyThreadPoolFactory implements ThreadFactory {
 ​
     private AtomicInteger threadNumber = new AtomicInteger(1);
     
     private ThreadGroup group;
 ​
     private String namePrefix = "";
 ​
     public MyThreadPoolFactory(String group) {
         this.group = new ThreadGroup(group);
         namePrefix = group + "-thread-pool-";
     }
 ​
 ​
     @Override
     public Thread newThread(Runnable r) {
         Thread t = new Thread(group, r,
                 namePrefix + threadNumber.getAndIncrement(),
                 0);
         t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
             @Override
             public void uncaughtException(Thread t, Throwable e) {
                 System.out.println(t.getName() + ":" + e);
             }
         });
 ​
         if (t.isDaemon()) {
             t.setDaemon(false);
         }
         if (t.getPriority() != Thread.NORM_PRIORITY) {
             t.setPriority(Thread.NORM_PRIORITY);
         }
         return t;
     }
 ​
 }

スレッドプールを閉じる

スレッド プールを閉じるには 2 つの方法があります。shutdown(),shutdownNow()

その原則は、ワーク キュー ワーカー内のスレッドを横断し、1 つずつ割り込み (スレッドのinterruptメソッドを呼び出す)、割り込みに応答できないタスクは決して終了しないことです。

シャットダウンタスクが実行されます

  1. スレッドプールのステータスを SHUTDOWN に設定します
  2. タスクを実行していないすべてのスレッドを中断します。

shutdownNow タスクが完了しない可能性があります

  1. スレッドプールのステータスを STOP に設定する
  2. タスクを実行または一時停止しているすべてのスレッドを停止してみます。
  3. 実行待ちタスクのリストに戻る

通常は Shutdown を使用しますが、タスクを完了する必要がない場合は shutdownNow を使用できます。

SecheduledThreadPoolExecutor

ScheduledThreadPoolExecutorThreadPoolExecutor基づいたスケジュール実行機能を提供します。

2つのタイミング方法があります

scheduleAtFixedRateたとえば、タスクの開始をサイクルの開始点とすると、タスクの実行には 0.5 秒かかり、1 秒ごとに実行されます。これは、タスクの完了後 0.5 秒後にタスクを開始することと同等です。

scheduledWithFixedDelayたとえば、タスクの終了をサイクルの開始点とすると、タスクの実行には 0.5 秒かかり、1 秒ごとに実行されます。これは、タスクが完了してから 1 秒後にタスクを開始することと同等です。

         ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(2);
         //scheduleAtFixedRate 固定频率执行任务 周期起点为任务开始
         scheduledThreadPoolExecutor.scheduleAtFixedRate(()->{
             try {
                 TimeUnit.SECONDS.sleep(1);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println("scheduleAtFixedRate 周期起点为任务开始");
             //初始延迟:1s  周期:1s
         },1,1, TimeUnit.SECONDS);
 ​
         //scheduledWithFixedDelay 固定延迟执行任务,周期起点为任务结束
         scheduledThreadPoolExecutor.scheduleWithFixedDelay(()->{
             try {
                 TimeUnit.SECONDS.sleep(1);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println("scheduledWithFixedDelay 周期起点为任务结束 ");
             //初始延迟:1s  周期:1s
         },1,1, TimeUnit.SECONDS);

タイミング スレッド プールは、遅延キューを使用してブロッキング キューとして機能します。

遅延キューはスケジュールされたタスクを分類して格納する優先キューで、時間が短いほど早く実行されます。

スレッドがタスクを取得すると、遅延キューからスケジュールされたタスクを取得し、時間が経過するとそれを実行します。

     public RunnableScheduledFuture<?> take() throws InterruptedException {
             final ReentrantLock lock = this.lock;
             lock.lockInterruptibly();
             try {
                 for (;;) {
                     RunnableScheduledFuture<?> first = queue[0];
                     //没有定时任务 等待
                     if (first == null)
                         available.await();
                     else {
                         //获取延迟时间
                         long delay = first.getDelay(NANOSECONDS);
                         //小于等于0 说明超时,拿出来执行
                         if (delay <= 0)
                             return finishPoll(first);
                         first = null; // don't retain ref while waiting
                         //当前线程是leader则等待对应的延迟时间,再进入循环取出任务执行
                         //不是leader则一直等待,直到被唤醒
                         if (leader != null)
                             available.await();
                         else {
                             Thread thisThread = Thread.currentThread();
                             leader = thisThread;
                             try {
                                 available.awaitNanos(delay);
                             } finally {
                                 if (leader == thisThread)
                                     leader = null;
                             }
                         }
                     }
                 }
             } finally {
                 if (leader == null && queue[0] != null)
                     available.signal();
                 lock.unlock();
             }
         }

これら 2 つのタイミング方法の 1 つはタスクの開始をサイクルの開始点とし、もう 1 つはタスクの終了をサイクルの開始点とします。

スケジュールされたタスクを取得するプロセスは、構築されるスケジュールされたタスクの遅延時間が異なることを除いて同じです。

スケジュールされたタスクの使用のperiod違いは、期間の開始点が正の数の場合はタスクの開始点であり、負の数の場合は期間の開始点がタスクの終了であることです。

要約する

この記事では、スレッド プールに焦点を当て、プーリング テクノロジ、Executor、スレッド プールのパラメータ、構成、実装原則、例外処理、シャットダウンなどについて簡単に説明します。

プーリング技術を使用すると、頻繁な作成と終了のオーバーヘッドを節約し、応答速度を向上させ、管理を容易にすることができ、スレッド プールや接続プールなどでよく使用されます。

Executor フレームワークは、作業タスクを実行 (スレッド プール) から切り離して分離し、戻り値のない作業タスクRunnableと戻り値のある作業タスクに分けます。Callable

Executor は実際にはRunnableタスクを処理するだけであり、タスクを適応型実行Callableにカプセル化します。FutureTaskRunnable

スレッド プールはワーク キューを使用してスレッドを管理します。スレッドがタスクを実行した後、ブロッキング キューからタスクを取得して実行します。非コア スレッドが一定時間アイドル状態になると、非コア スレッドは閉じられます。

スレッド プールの実行時に、ワーク キュー スレッドの数がコア スレッドの数より少ない場合は、実行するためにスレッドが作成されます (かなりウォームアップされます)。

ワーク キュー スレッドの数がコア スレッドの数より大きく、ブロッキング キューがいっぱいでない場合、ワーク キューはブロッキング キューに配置されます。

ブロッキング キューがいっぱいで、スレッドの最大数に達していない場合は、タスクを実行するために非コア スレッドが作成されます。

スレッドの最大数に達した場合は拒否ポリシーを使用する

構成パラメータ: CPU 集中型は CPU コア数 + 1、IO 集中型は CPU コア数の 2 倍、特定の構成をテストする必要があります。

例外を処理するには、タスクを直接キャプチャするか、Callableget をキャプチャするか、スレッド プールを継承してafterExecutor例外を記録するか、スレッドの作成時にキャッチされない例外を処理するメソッドを設定することができます。

スケジュールされたタスクを処理するスレッド プールは遅延キューによって実装されます。スケジュールされたタスクが短いほど、より早く実行されます。スレッドは (時間が経過すると) 遅延キューからスケジュールされたタスクを取得し、時間が経過するまで待機します。起きています。

最後に(ただでやらないで、助けを求めるために連続3回押してください〜)

この記事は、Java コンカレント プログラミングの知識体系をわかりやすく構築するためのコラム「点から線、線から面へ」に収録されていますので、興味のある方は引き続き注目してください。

この記事のメモとケースはgitee-StudyJavaおよびgithub-StudyJavaに含まれています。興味のある学生は stat~ で引き続き注目してください。

ケースの住所:

Gitee-JavaConcurrentProgramming/src/main/java/D_ThreadPool

Github-JavaConcurrentProgramming/src/main/java/D_ThreadPool

質問がある場合は、コメント欄で話し合ってください。Cai Cai の文章が良いと思う場合は、いいね、フォロー、収集してサポートしてください~

Cai Cai をフォローして、より有益な情報を共有してください。公開アカウント: Cai Cai のバックエンド プライベート キッチン

この記事は、複数の記事を公開するブログOpenWriteによって公開されています。

雷軍氏: Xiaomi の新オペレーティング システム ThePaper OS の正式版がパッケージ化されました Gome App の宝くじページのポップアップ ウィンドウが創設者を侮辱 米 政府が NVIDIA H800 GPU の中国への輸出を制限 Xiaomi ThePaper OS インターフェース マスターが Scratch を使用して RISC-V シミュレータを操作し、正常に実行されました Linux カーネル RustDesk リモート デスクトップ 1.2.3 がリリースされ、Wayland サポートが強化されました Logitech USB レシーバーを取り外した後、Linux カーネルがクラッシュしました DHH の「パッケージング ツール」のシャープ レビュー": フロントエンドはまったくビルドする必要がありません (No Build) JetBrains が技術文書を作成するために Writerside を起動 Node.js 21 用ツールが正式リリース
{{名前}}
{{名前}}

おすすめ

転載: my.oschina.net/u/6903207/blog/10109044