スレッドプール - ThreadPoolExecutor の詳細な説明
スレッド プール ThreadPoolExecutor のソース コード分析については、juc コラム シリーズの記事を参照してください。
基本的な概要
スレッド プール: 複数のスレッドを保持するコンテナ。コンテナ内のスレッドは再利用できるため、スレッド オブジェクトを頻繁に作成および破棄する必要がなくなります。
スレッドプールの役割:
- リソース消費を削減し、作成および破棄されるスレッドの数を減らし、各ワーカー スレッドを再利用して複数のタスクを実行できます。
- 応答速度の向上 タスク到着時に直接使用できるスレッドがあればシステムがフリーズしません。
- スレッドの管理性を向上します。無制限にスレッドを作成すると、システム リソースを消費するだけでなく、システムの安定性も低下します。スレッド プールを使用することで、一元的な割り当て、チューニング、監視が可能になります。
スレッド プールの中心となるアイデアは、スレッドの再利用です。同じスレッドを再利用して複数のタスクを処理できます。
プーリング テクノロジ (プール): プログラミング手法の 1 つであり、その中心的な考え方はリソースの再利用です。これにより、リクエストが多数ある場合にアプリケーションのパフォーマンスを最適化し、頻繁なシステム接続確立によるリソースのオーバーヘッドを削減できます。
カスタムスレッドプール
説明する
このコードは、単純なスレッド プールを実装します。つまり
只实现了核心线程数,没有实现最大线程数
、スレッド プール内のスレッド数が coreSize に達すると、新しいタスクがキューに直接入れられ、キューがいっぱいの場合は、拒否戦略が直接使用されます。スレッドの最大数 maxSize が設定されていません。
1) カスタム拒否ポリシーインターフェイス
- 設計パターン -
策略模式
: 特定の操作をインターフェイスに抽象化し、特定の実装が呼び出し元によって渡されます。
// 拒绝策略
@FunctionalInterface
interface RejectPolicy<T> {
void reject(BlockingQueue<T> queue, T task);
}
2) カスタムタスクキュー
// 阻塞队列 用来协调生产者与消费者
class BlockingQueue<T> {
// 1.任务队列
private Deque<T> queue = new ArrayDeque<>();
// 2.锁
private ReentrantLock lock = new ReentrantLock();
// 3.生产者条件变量
private Condition fullWaitSet = lock.newCondition();
// 4.消费者条件变量
private Condition emptyWaitSet = lock.newCondition();
// 5.容量
private int capcity;
public BlockingQueue(int capcity) {
this.capcity = capcity;
}
// 带超时阻塞获取
public T poll(long timeout, TimeUnit unit) {
lock.lock();
try {
// 将 timeout 统一转换为 纳秒
long nanos = unit.toNanos(timeout);
while (queue.isEmpty()) {
try {
// 返回值是剩余时间
if (nanos <= 0) {
return null;
}
nanos = emptyWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal();
return t;
} finally {
lock.unlock();
}
}
// 阻塞获取
public T take() {
lock.lock();
try {
while (queue.isEmpty()) {
try {
emptyWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal();
return t;
} finally {
lock.unlock();
}
}
// 阻塞添加
public void put(T task) {
lock.lock();
try {
while (queue.size() == capcity) {
try {
log.debug("等待加入任务队列 {} ...", task);
fullWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("加入任务队列 {}", task);
queue.addLast(task);
emptyWaitSet.signal();
} finally {
lock.unlock();
}
}
// 带超时时间阻塞添加
public boolean offer(T task, long timeout, TimeUnit timeUnit) {
lock.lock();
try {
long nanos = timeUnit.toNanos(timeout);
while (queue.size() == capcity) {
try {
if (nanos <= 0) {
return false;
}
log.debug("等待加入任务队列 {} ...", task);
nanos = fullWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("加入任务队列 {}", task);
queue.addLast(task);
emptyWaitSet.signal();
return true;
} finally {
lock.unlock();
}
}
public int size() {
lock.lock();
try {
return queue.size();
} finally {
lock.unlock();
}
}
public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
lock.lock();
try {
// 判断队列是否满
if (queue.size() == capcity) {
// 要执行的拒绝策略
rejectPolicy.reject(this, task);
} else {
// 有空闲
log.debug("加入任务队列 {}", task);
queue.addLast(task);
emptyWaitSet.signal();
}
} finally {
lock.unlock();
}
}
}
3) カスタムスレッドプール
class ThreadPool {
// 任务队列
private BlockingQueue<Runnable> taskQueue;
// 线程集合
private HashSet<Worker> workers = new HashSet<>();
// 核心线程数
private int coreSize;
// 获取任务时的超时时间
private long timeout;
private TimeUnit timeUnit;
// 拒绝策略
private RejectPolicy<Runnable> rejectPolicy;
// 执行任务
public void execute(Runnable task) {
// 当任务数没有超过 coreSize 时,直接交给 worker 对象执行
// 如果任务数超过 coreSize 时,加入任务队列暂存
synchronized (workers) {
if (workers.size() < coreSize) {
Worker worker = new Worker(task);
log.debug("新增 worker{}, {}", worker, task);
workers.add(worker);
worker.start();
} else {
//taskQueue.put(task);
// 1) 死等
// 2) 带超时等待
// 3) 让调用者放弃任务执行
// 4) 让调用者抛出异常
// 5) 让调用者自己执行任务
// 策略模式-把具体的操作抽象成接口,具体的实现由调用者传递进来
taskQueue.tryPut(rejectPolicy, task);
}
}
}
public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapcity, RejectPolicy<Runnable> rejectPolicy) {
this.coreSize = coreSize;
this.timeout = timeout;
this.timeUnit = timeUnit;
this.taskQueue = new BlockingQueue<>(queueCapcity);
// 拒绝策略的具体的实现通过调用者使用构造方法传递进来
this.rejectPolicy = rejectPolicy;
}
class Worker extends Thread {
private Runnable task;
public Worker(Runnable task) {
this.task = task;
}
@Override
public void run() {
// 执行任务
// 1) 当 task 不为空,执行任务
// 2) 当 task 执行完毕,再接着从任务队列获取任务并执行
//while(task != null || (task = taskQueue.take()) != null) {
while (task != null || (task = taskQueue.poll(timeout, timeUnit)) != null) {
try {
log.debug("正在执行...{}", task);
task.run();
} catch (Exception e) {
e.printStackTrace();
} finally {
task = null;
}
}
synchronized (workers) {
log.debug("worker 被移除{}", this);
workers.remove(this);
}
}
}
}
4) テスト
public class TestPool {
public static void main(String[] args) {
ThreadPool threadPool = new ThreadPool(1,
1000, TimeUnit.MILLISECONDS, 1, (queue, task) -> {
// 调用者选择拒绝策略
// 1) 死等
//queue.put(task);
// 2) 带超时等待
//queue.offer(task, 1500, TimeUnit.MILLISECONDS);
// 3) 让调用者放弃任务执行
//log.debug("放弃{}", task);
// 4) 让调用者抛出异常
//throw new RuntimeException("任务执行失败 " + task);
// 5) 让调用者自己执行任务
task.run();
});
for (int i = 0; i < 4; i++) {
int j = i;
threadPool.execute(() -> {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("{}", j);
});
}
}
}
拒否ポリシーのデモンストレーション効果
より便利なデモンストレーション効果を得るために、ブロッキング キューのコア スレッドの最大数を 1 に設定します。つまり、キューに格納できるスレッドは 1 つだけです。
-
1) 死んで待つ
時間のかかるタスクの実行 (コード内のスリープ スリープ時間) を増やして、タスクの実行が完了するまで待機してデッド ウェイティングの効果を実現する必要があります。
18:05:24.718 c.ThreadPool [main] - 新增 workerThread[Thread-0,5,main], cn.itcast.n8.TestPool$$Lambda$2/245672235@66d33a 18:05:24.722 c.BlockingQueue [main] - 加入任务队列 cn.itcast.n8.TestPool$$Lambda$2/245672235@2c8d66b2 18:05:24.722 c.ThreadPool [Thread-0] - 正在执行...cn.itcast.n8.TestPool$$Lambda$2/245672235@66d33a 18:05:24.722 c.BlockingQueue [main] - 等待加入任务队列 cn.itcast.n8.TestPool$$Lambda$2/245672235@5a39699c ...
-
2) タイムアウトして待つ
合計 3 つのタスクが実行され、タイムアウトによる待機タイムアウト期間は 500 ミリ秒、タスクの実行時間は 1000 ミリ秒かかります。
プログラムが終了を待っていないことがわかります。タスク 0 が最初に実行され、タスク 1 が待機キューに追加されます。1 秒後にタスク 0 が実行され、1 秒後にタスク 1 も実行されますが、タスク 2 は実行されません。追加処理中にタイムアウトを待機しており、ブロッキング キューに追加されていないため、タスク 2 は実行されません。
18:10:40.295 c.ThreadPool [main] - 新增 workerThread[Thread-0,5,main], cn.itcast.n8.TestPool$$Lambda$2/245672235@66d33a 18:10:40.298 c.BlockingQueue [main] - 加入任务队列 cn.itcast.n8.TestPool$$Lambda$2/245672235@2c8d66b2 18:10:40.298 c.BlockingQueue [main] - 等待加入任务队列 cn.itcast.n8.TestPool$$Lambda$2/245672235@5a39699c ... 18:10:40.298 c.ThreadPool [Thread-0] - 正在执行...cn.itcast.n8.TestPool$$Lambda$2/245672235@66d33a 18:10:41.311 c.TestPool [Thread-0] - 0 18:10:41.311 c.ThreadPool [Thread-0] - 正在执行...cn.itcast.n8.TestPool$$Lambda$2/245672235@2c8d66b2 18:10:42.317 c.TestPool [Thread-0] - 1 18:10:43.321 c.ThreadPool [Thread-0] - worker 被移除Thread[Thread-0,5,main]
-
3) 呼び出し元にタスクの実行を放棄させます。
何も書かないということは諦めることだ。ログを使用して段落を出力できます。
合計 3 つのタスクが実行されましたが、キューが出現するとすぐにいっぱいになったため、タスク 2 は実行を直接諦め、タスク 0 と 1 のみが実行されました。
18:19:41.920 c.ThreadPool [main] - 新增 workerThread[Thread-0,5,main], cn.itcast.n8.TestPool$$Lambda$2/245672235@66d33a 18:19:41.924 c.BlockingQueue [main] - 加入任务队列 cn.itcast.n8.TestPool$$Lambda$2/245672235@2c8d66b2 18:19:41.925 c.ThreadPool [Thread-0] - 正在执行...cn.itcast.n8.TestPool$$Lambda$2/245672235@66d33a 18:19:41.925 c.TestPool [main] - 放弃cn.itcast.n8.TestPool$$Lambda$2/245672235@5a39699c 18:19:42.931 c.TestPool [Thread-0] - 0 18:19:42.932 c.ThreadPool [Thread-0] - 正在执行...cn.itcast.n8.TestPool$$Lambda$2/245672235@2c8d66b2 18:19:43.941 c.TestPool [Thread-0] - 1 18:19:44.951 c.ThreadPool [Thread-0] - worker 被移除Thread[Thread-0,5,main]
-
4) 呼び出し元に例外をスローさせます
例外をスローすると残りのタスクが実行されなくなるため、このときタスク数を4に調整します。
2 番目のタスクの実行時に例外がスローされたため、3 番目のタスクはまったく実行されませんでした。
18:47:31.348 c.ThreadPool [main] - 新增 workerThread[Thread-0,5,main], cn.itcast.n8.TestPool$$Lambda$2/245672235@66d33a 18:47:31.352 c.BlockingQueue [main] - 加入任务队列 cn.itcast.n8.TestPool$$Lambda$2/245672235@2c8d66b2 18:47:31.352 c.ThreadPool [Thread-0] - 正在执行...cn.itcast.n8.TestPool$$Lambda$2/245672235@66d33a Exception in thread "main" java.lang.RuntimeException: 任务执行失败cn.itcast.n8.TestPool$$Lambda$2/245672235@5a39699c at cn.itcast.n8.TestPool.lambda$main$0(TestPool.java:25) at cn.itcast.n8.BlockingQueue.tryPut(TestPool.java:250) at cn.itcast.n8.ThreadPool.execute(TestPool.java:83) at cn.itcast.n8.TestPool.main(TestPool.java:31) 18:47:32.353 c.TestPool [Thread-0] - 0 18:47:32.354 c.ThreadPool [Thread-0] - 正在执行...cn.itcast.n8.TestPool$$Lambda$2/245672235@2c8d66b2 18:47:33.363 c.TestPool [Thread-0] - 1 18:47:34.375 c.ThreadPool [Thread-0] - worker 被移除Thread[Thread-0,5,main]
-
5) 呼び出し元にタスク自体を実行させる
タスクの run() メソッドを直接呼び出すと、実際にはメインスレッド自体によって実行されます。
タスク0と1は[Thread-0]で実行され、タスク2と3はメインスレッドmainで実行され、合計4つのタスクが実行されます。
18:58:03.790 c.ThreadPool [main] - 新增 workerThread[Thread-0,5,main], cn.itcast.n8.TestPool$$Lambda$2/245672235@66d33a 18:58:03.794 c.BlockingQueue [main] - 加入任务队列 cn.itcast.n8.TestPool$$Lambda$2/245672235@2c8d66b2 18:58:03.794 c.ThreadPool [Thread-0] - 正在执行...cn.itcast.n8.TestPool$$Lambda$2/245672235@66d33a 18:58:04.800 c.TestPool [Thread-0] - 0 18:58:04.800 c.TestPool [main] - 2 18:58:05.810 c.TestPool [main] - 3 18:58:05.810 c.ThreadPool [Thread-0] - 正在执行...cn.itcast.n8.TestPool$$Lambda$2/245672235@2c8d66b2 18:58:06.814 c.TestPool [Thread-0] - 1 18:58:07.817 c.ThreadPool [Thread-0] - worker 被移除Thread[Thread-0,5,main]
スレッドプールエグゼキュータ
スレッド プール内のスレッドはすべて非デーモン スレッドです。
1) スレッドプールのステータス
ThreadPoolExecutor は、int の上位 3 ビットを使用してスレッド プールのステータスを表し、下位 29 ビットを使用してスレッドの数を表します。
州名 | 上位 3ビット | 新しいタスク | ブロッキングキュータスク | 説明する |
---|---|---|---|---|
RUNNING |
111 | Y | Y | |
SHUTDOWN |
000 | N | Y | 新しいタスクは受け入れられませんが、ブロックキュー内の残りのタスクは処理されます |
STOP |
001 | N | N | 実行中のタスクを中断し、ブロックしているキューのタスクを破棄します。 |
TIDYING |
010 | - | - | タスクは完全に実行され、アクティブなスレッドは 0 で、まさに終わりに近づきます。 |
TERMINATED |
011 | - | - | 端末状態 |
数値的に比較してみると、TERMINATED
> TIDYING
> STOP
> SHUTDOWN
>RUNNING
これらの情報はアトミック変数 ctl に保存されます。その目的は、スレッド プールの状態とスレッドの数を 1 つに結合して、cas アトミック操作を割り当てに使用できるようにすることです。
// c 为旧值, ctlOf 返回结果为新值
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))));
// rs 为高 3 位代表线程池状态, wc 为低 29 位代表线程个数,ctl 是合并它们
private static int ctlOf(int rs, int wc) {
return rs | wc; }
2) 施工方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize コアスレッド数(予約スレッドの最大数)
- minimumPoolSize スレッドの最大数
- keepAliveTime 生存時間 - 緊急スレッド用
- 単位時間単位 - レスキュースレッド用
- workQueue ブロックキュー
- threadFactory スレッド ファクトリ - スレッドの作成時に適切な名前を付けることができます
- ハンドラー拒否ポリシー
作業の方法:
- スレッド プールには最初はスレッドがありませんが、タスクがスレッド プールに投入されると、スレッド プールはタスクを実行するための新しいスレッドを作成します。
- スレッド数が corePoolSize に達し、アイドル状態のスレッドがない場合は、この時点でタスクを追加します。アイドル状態のスレッドがなくなるまで、新しく追加されたタスクは workQueue キューに追加されます。
- キューがバウンドキューを選択している場合、タスクがキューサイズを超えた場合、緊急用にmaximumPoolSize - corePoolSize番号のスレッドが作成されます。
- スレッドがmaximumPoolSizeに達し、まだ新しいタスクがある場合、この時点で拒否ポリシーが実行されます。
- 拒否戦略 jdk は 4 つの実装を提供します。
- AbortPolicy を使用すると、呼び出し元はデフォルトのポリシーである RejectedExecutionException をスローできます。
- CallerRunsPolicy により、呼び出し元がタスクを実行できるようになります
- DiscardPolicy はこのタスクを放棄します
- DiscardOldestPolicy は、キュー内の最も古いタスクを破棄し、このタスクに置き換えます。
- 他のよく知られたフレームワークでも実装が提供されています。
- Dubbo の実装は、RejectedExecutionException 例外をスローする前にログを記録し、問題の特定を容易にするためにスレッド スタック情報をダンプします。
- Netty の実装では、タスクを実行するための新しいスレッドを作成します。
- 以前のカスタム拒否戦略と同様に、キューへの投入を試行する際にタイムアウト待機 (60 秒) を伴う ActiveMQ の実装
- チェーン内の各拒否ポリシーを 1 つずつ試行する拒否ポリシーのチェーンを使用する PinPoint の実装
- 拒否戦略 jdk は 4 つの実装を提供します。
- ピーク値が過ぎたとき、corePoolSize を超える緊急スレッドが一定期間実行するタスクがない場合、リソースを節約するためにスレッドを終了する必要があります。この時間は keepAliveTime と単位によって制御されます。
この構築方法に従って、さまざまな目的のスレッド プールを作成するための多くのファクトリ メソッドが JDK Executor クラスで提供されます。
3) 新しい固定スレッドプール
固定サイズのスレッド プールを作成する
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
特徴
核心线程数 == 最大线程数
(レスキュー スレッドは作成されない)、タイムアウトの必要はありません。- ブロッキング キューには制限がなく、任意の数のタスクを保持できます。
評価
タスク量が既知のタスクや比較的時間のかかるタスクに適用可能
4) 新しいキャッシュされたスレッドプール
バッファリングされたスレッドプール
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
特徴
- コアスレッドの数は 0、スレッドの最大数は Integer.MAX_VALUE、緊急スレッドのアイドルライフタイムは 60 秒です。つまり、
- すべて緊急スレッドです (60 秒後にリサイクル可能)
- 緊急スレッドは無限に作成可能
- キューはSynchronousQueueを採用し、容量がなく、取得するスレッドが無いと入れられない特性を実現(片手決済、片手配送)
SynchronousQueue<Integer> integers = new SynchronousQueue<>();
new Thread(() -> {
try {
log.debug("putting {} ", 1);
integers.put(1);
log.debug("{} putted...", 1);
log.debug("putting...{} ", 2);
integers.put(2);
log.debug("{} putted...", 2);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1").start();
sleep(1);
new Thread(() -> {
try {
log.debug("taking {}", 1);
integers.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t2").start();
sleep(1);
new Thread(() -> {
try {
log.debug("taking {}", 2);
integers.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t3").start();
出力
11:48:15.500 c.TestSynchronousQueue [t1] - putting 1
11:48:16.500 c.TestSynchronousQueue [t2] - taking 1
11:48:16.500 c.TestSynchronousQueue [t1] - 1 putted...
11:48:16.500 c.TestSynchronousQueue [t1] - putting...2
11:48:17.502 c.TestSynchronousQueue [t3] - taking 2
11:48:17.503 c.TestSynchronousQueue [t1] - 2 putted...
評価
スレッドプール全体はタスク量に応じてスレッド数が増加し続け、上限はなく、タスクが実行されると1分間アイドル状態になった後にスレッドが解放されます。
タスクの数は比較的集中しているが、各タスクの実行時間は比較的短い状況に適しています。
5) newSingleThreadExecutor
シングルスレッドのスレッドプール
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
使用するシーン:
- 複数のタスクが実行のためにキューに入れられることが予想されます。スレッド数は1固定で、タスク数が1を超える場合は無制限キューにキューイングされます。タスクの実行後、唯一のスレッドは解放されません。
違い:
- シングルスレッドのシリアル実行タスクを自分で作成します。タスクの実行が失敗して終了した場合、対処法はなく、スレッド プールはプールの正常な動作を保証するために新しいスレッドも作成します。
- Executors.newSingleThreadExecutor() スレッドの数は常に 1 であり、変更できません
- FinalizableDelegatedExecutorService はデコレータ モードを適用し、ExecutorService インターフェイスのみを公開するため、ThreadPoolExecutor 内の固有のメソッドを呼び出すことはできません。
- Executors.newFixedThreadPool(1) は最初は 1 ですが、後で変更できます。
- 外部に公開されるのは ThreadPoolExecutor オブジェクトで、強制転送後に setCorePoolSize やその他のメソッドを呼び出すことで変更できます。
6) タスクを送信する
// 执行任务
void execute(Runnable command);
// 提交任务 task,用返回值 Future 获得任务执行结果
<T> Future<T> submit(Callable<T> task);
// 提交 tasks 中所有任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;
// 提交 tasks 中所有任务,带超时时间
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException;
// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException;
// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
7) スレッドプールを閉じる
-
シャットダウン
/* 线程池状态变为 SHUTDOWN - 不会接收新任务 - 但已提交任务会执行完 - 此方法不会阻塞调用线程的执行 */ void shutdown();
public void shutdown() { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess(); // 修改线程池状态 advanceRunState(SHUTDOWN); // 仅会打断空闲线程 interruptIdleWorkers(); onShutdown(); // 扩展点 ScheduledThreadPoolExecutor } finally { mainLock.unlock(); } // 尝试终结(没有运行的线程可以立刻终结,如果还有运行的线程也不会等) tryTerminate(); }
-
今すぐシャットダウン
/* 线程池状态变为 STOP - 不会接收新任务 - 会将队列中的任务返回 - 并用 interrupt 的方式中断正在执行的任务 */ List<Runnable> shutdownNow();
public List<Runnable> shutdownNow() { List<Runnable> tasks; final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess(); // 修改线程池状态 advanceRunState(STOP); // 打断所有线程 interruptWorkers(); // 获取队列中剩余任务 tasks = drainQueue(); } finally { mainLock.unlock(); } // 尝试终结 tryTerminate(); return tasks; }
-
他の方法
// 不在 RUNNING 状态的线程池,此方法就返回 true boolean isShutdown(); // 线程池状态是否是 TERMINATED boolean isTerminated(); // 调用 shutdown 后,由于调用线程并不会等待所有任务运行结束,因此如果它想在线程池 TERMINATED 后做些事情,可以利用此方法等待 boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
非同期モードのワーカー スレッド
ワーカースレッドモード
意味
制限されたワーカー スレッド (ワーカー スレッド) が交代で無制限のタスクを非同期に処理できるようにします。これは分業モードとしても分類でき、典型的な実装はスレッド プールであり、クラシック デザイン モードのフライウェイト モードも反映されます。
たとえば、Haidilao のウェイター (スレッド) は各ゲストの注文 (タスク) を順番に処理しますが、各ゲストに専用のウェイターが割り当てられると、コストが高くなりすぎます (別のマルチスレッド設計パターン: Thread-Per-Message と比較して)。
異なるタスクタイプは異なるスレッドプールを使用する必要があることに注意してください。これにより、枯渇を回避し、効率を向上させることができます。
たとえば、レストランの従業員がゲストを出迎え (タスク タイプ A)、裏のキッチンに行って調理する必要がある場合 (タスク タイプ B) は明らかに効率が悪いため、従業員をウェイター (スレッド プール A) とウェイター (スレッド プール A) に分けるのはより困難です。シェフ (スレッド プール B) 当然のことながら、より詳細な分業を考えることもできます。
飢え
固定サイズのスレッド プールが不足します。
- 2 つのワーカーは同じスレッド プール内の 2 つのスレッドです
- 彼らがしなければならないことは、ゲストに注文を出し、キッチンで調理するという 2 段階の仕事です。
- ゲストは食べ物を注文します。最初に食べ物を注文し、食べ物が準備できるのを待って、食べ物を提供する必要があります。この間、注文を処理する従業員は待機する必要があります。
- 裏のキッチンで料理:何も言うことはありません、ただやってください
- たとえば、従業員 A が料理の注文を担当し、従業員 B が料理を準備して提供するのを待つ必要があります。
- しかし、今度は 2 人の客が同時に来ます。この時点では、従業員 A と従業員 B の両方が注文を処理する予定です。この時点では、誰も料理をしていなくて、お腹が空いています。
public class TestDeadLock {
static final List<String> MENU = Arrays.asList("地三鲜", "宫保鸡丁", "辣子鸡丁", "烤鸡翅");
static Random RANDOM = new Random();
static String cooking() {
return MENU.get(RANDOM.nextInt(MENU.size()));
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.execute(() -> {
log.debug("处理点餐...");
Future<String> f = executorService.submit(() -> {
log.debug("做菜");
return cooking();
});
try {
log.debug("上菜: {}", f.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
/*executorService.execute(() -> {
log.debug("处理点餐...");
Future<String> f = executorService.submit(() -> {
log.debug("做菜");
return cooking();
});
try {
log.debug("上菜: {}", f.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});*/
}
}
出力
17:21:27.883 c.TestDeadLock [pool-1-thread-1] - 处理点餐...
17:21:27.891 c.TestDeadLock [pool-1-thread-2] - 做菜
17:21:27.891 c.TestDeadLock [pool-1-thread-1] - 上菜: 烤鸡翅
コメントを解除した場合に考えられる出力
17:08:41.339 c.TestDeadLock [pool-1-thread-2] - 处理点餐...
17:08:41.339 c.TestDeadLock [pool-1-thread-1] - 处理点餐...
この時点でスターベーションが発生し、JConsole 経由でデッドロックは検出されません。
現在の飢餓現象をどう解決するか?
スレッド プールのサイズを増やすことはできますが、根本的な解決策ではありません。前述したように、タスクの種類が異なれば、使用するスレッド プールも異なります。たとえば、次のようになります。
public class TestDeadLock {
static final List<String> MENU = Arrays.asList("地三鲜", "宫保鸡丁", "辣子鸡丁", "烤鸡翅");
static Random RANDOM = new Random();
static String cooking() {
return MENU.get(RANDOM.nextInt(MENU.size()));
}
public static void main(String[] args) {
ExecutorService waiterPool = Executors.newFixedThreadPool(1);
ExecutorService cookPool = Executors.newFixedThreadPool(1);
waiterPool.execute(() -> {
log.debug("处理点餐...");
Future<String> f = cookPool.submit(() -> {
log.debug("做菜");
return cooking();
});
try {
log.debug("上菜: {}", f.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
waiterPool.execute(() -> {
log.debug("处理点餐...");
Future<String> f = cookPool.submit(() -> {
log.debug("做菜");
return cooking();
});
try {
log.debug("上菜: {}", f.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
}
}
出力
17:25:14.626 c.TestDeadLock [pool-1-thread-1] - 处理点餐...
17:25:14.630 c.TestDeadLock [pool-2-thread-1] - 做菜
17:25:14.631 c.TestDeadLock [pool-1-thread-1] - 上菜: 地三鲜
17:25:14.632 c.TestDeadLock [pool-1-thread-1] - 处理点餐...
17:25:14.632 c.TestDeadLock [pool-2-thread-1] - 做菜
17:25:14.632 c.TestDeadLock [pool-1-thread-1] - 上菜: 辣子鸡丁
作成するのに適切なスレッド プールの数はいくつですか
- 小さすぎると、プログラムがシステム リソースを十分に活用できなくなり、飢餓が発生しやすくなります。
- 大きすぎると、より多くのスレッド コンテキストの切り替えが発生し、より多くのメモリを消費します。
CPU 負荷の高い操作
- 通常、
cpu 核数 + 1
最適な CPU 使用率を達成するために使用されます。+1 は、ページ欠落障害 (オペレーティング システム) またはその他の理由でスレッドが中断されたときに、CPU クロック サイクルが無駄にならないように追加のスレッドを補充できるようにするためです。
I/O 集中型の操作
- CPU は常にビジー状態にあるわけではありません。たとえば、ビジネス計算を実行するときはこの時点で CPU リソースが使用されますが、データベース操作を含む I/O 操作やリモート RPC 呼び出しを実行するときは、CPU はアイドル状態になります。 time Down の場合は、マルチスレッドを使用して使用率を向上させることができます。
- 実験式は次のとおりです
线程数 = 核数 * 期望 CPU 利用率 * 总时间(CPU计算时间+等待时间) / CPU 计算时间
- たとえば、4 コア CPU の計算時間が 50%、その他の待ち時間が 50%、予想される CPU が 100% 使用されている場合、次の式を適用します。
4 * 100% * 100% / 50% = 8
- 例えば、4コアCPUの計算時間は10%、その他の待ち時間は90%で、CPUが100%使用されると予想され、計算式が適用されます。
4 * 100% * 100% / 10% = 40
カスタムスレッドプール
上記で実装されました。
8) タスクスケジューリングスレッドプール
「タスクスケジューリングスレッドプール」機能を追加する前に、java.util.Timerを使用してタイミング機能を実装できます。Timerの利点は使いやすいことですが、すべてのタスクが同じスレッドによってスケジュールされるため、すべてのタスクがタスクはシリアル化されており、同時に実行できるタスクは 1 つだけであり、前のタスクの遅延や例外は後続のタスクに影響します。
public static void main(String[] args) {
Timer timer = new Timer();
TimerTask task1 = new TimerTask() {
@Override
public void run() {
log.debug("task 1");
sleep(2);
}
};
TimerTask task2 = new TimerTask() {
@Override
public void run() {
log.debug("task 2");
}
};
// 使用 timer 添加两个任务,希望它们都在 1s 后执行
// 但由于 timer 内只有一个线程来顺序执行队列中的任务,因此『任务1』的延时,影响了『任务2』的执行
timer.schedule(task1, 1000);
timer.schedule(task2, 1000);
}
出力
20:46:09.444 c.TestTimer [main] - start...
20:46:10.447 c.TestTimer [Timer-0] - task 1
20:46:12.448 c.TestTimer [Timer-0] - task 2
ScheduledExecutorService を使用して書き換えます。
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); // 如果线程池大小设置为 1 两个线程还是会串行执行
// 添加两个任务,希望它们都在 1s 后执行
executor.schedule(() -> {
System.out.println("任务1,执行时间:" + new Date());
//int i = 1 / 0; // 即使有异常 也不影响第二个线程的执行
try {
Thread.sleep(2000); } catch (InterruptedException e) {
}
}, 1000, TimeUnit.MILLISECONDS); // 参数:任务对象,延时时间,时间单位
executor.schedule(() -> {
System.out.println("任务2,执行时间:" + new Date());
}, 1000, TimeUnit.MILLISECONDS);
出力
任务1,执行时间:Thu Jan 03 12:45:17 CST 2019
任务2,执行时间:Thu Jan 03 12:45:17 CST 2019
scheduleAtFixedRate (スケジュールされた実行) の例:
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
log.debug("start...");
pool.scheduleAtFixedRate(() -> {
log.debug("running...");
}, 1, 1, TimeUnit.SECONDS); // 参数:任务对象,延时时间,执行的间隔时间,时间单位
出力
21:45:43.167 c.TestTimer [main] - start...
21:45:44.215 c.TestTimer [pool-1-thread-1] - running...
21:45:45.215 c.TestTimer [pool-1-thread-1] - running...
21:45:46.215 c.TestTimer [pool-1-thread-1] - running...
21:45:47.215 c.TestTimer [pool-1-thread-1] - running...
ScheduleAtFixedRate の例 (タスクの実行時間が間隔を超えた場合):
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
log.debug("start...");
pool.scheduleAtFixedRate(() -> {
log.debug("running...");
sleep(2);
}, 1, 1, TimeUnit.SECONDS);
出力分析: 最初は遅延は 1 秒ですが、タスクの実行時間 > 間隔時間であるため、間隔は 2 秒に「サポート」されます。
21:44:30.311 c.TestTimer [main] - start...
21:44:31.360 c.TestTimer [pool-1-thread-1] - running...
21:44:33.361 c.TestTimer [pool-1-thread-1] - running...
21:44:35.362 c.TestTimer [pool-1-thread-1] - running...
21:44:37.362 c.TestTimer [pool-1-thread-1] - running...
scheduleWithFixedDelay (実際の間隔時間) の例:
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
log.debug("start...");
pool.scheduleWithFixedDelay(()-> {
log.debug("running...");
sleep(2);
}, 1, 1, TimeUnit.SECONDS); // 参数:任务对象,延时时间,任务与任务之间真正的间隔时间,时间单位
出力分析: 開始時の遅延は 1 秒で、scheduleWithFixedDelay の間隔は前のタスクの終了 <-> 遅延 <-> 次のタスクの開始であるため、間隔は 3 秒です。
21:40:55.078 c.TestTimer [main] - start...
21:40:56.140 c.TestTimer [pool-1-thread-1] - running...
21:40:59.143 c.TestTimer [pool-1-thread-1] - running...
21:41:02.145 c.TestTimer [pool-1-thread-1] - running...
21:41:05.147 c.TestTimer [pool-1-thread-1] - running...
評価
スレッド プール全体のパフォーマンスは、スレッド数が固定されており、タスク数がスレッド数を超える場合、無制限のキューにキューイングされます。
タスクの実行後、これらのスレッドは解放されません。遅延または繰り返しのタスクを実行するために使用されます。
9) 実行タスクの例外を正しく処理する
-
方法 1: 例外を積極的にキャッチする
ExecutorService pool = Executors.newFixedThreadPool(1); pool.submit(() -> { try { log.debug("task1"); int i = 1 / 0; } catch (Exception e) { log.error("error:", e); } });
出力
21:59:04.558 c.TestTimer [pool-1-thread-1] - task1 21:59:04.562 c.TestTimer [pool-1-thread-1] - error: java.lang.ArithmeticException: / by zero at cn.itcast.n8.TestTimer.lambda$main$0(TestTimer.java:28) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)
-
方法 2: Future を使用する
ExecutorService pool = Executors.newFixedThreadPool(1); Future<Boolean> f = pool.submit(() -> { log.debug("task1"); int i = 1 / 0; return true; }); log.debug("result:{}", f.get());
出力
21:54:58.208 c.TestTimer [pool-1-thread-1] - task1 Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero at java.util.concurrent.FutureTask.report(FutureTask.java:122) at java.util.concurrent.FutureTask.get(FutureTask.java:192) at cn.itcast.n8.TestTimer.main(TestTimer.java:31) Caused by: java.lang.ArithmeticException: / by zero at cn.itcast.n8.TestTimer.lambda$main$0(TestTimer.java:28) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)
タイミングタスクアプリケーション
タスクを毎週木曜日の 18:00:00 に定期的に実行するにはどうすればよいですか?
// 获得当前时间
LocalDateTime now = LocalDateTime.now();
// 获取本周四 18:00:00.000
LocalDateTime thursday =
now.with(DayOfWeek.THURSDAY).withHour(18).withMinute(0).withSecond(0).withNano(0);
// 如果当前时间已经超过 本周四 18:00:00.000, 那么找下周四 18:00:00.000
if (now.compareTo(thursday) >= 0) {
thursday = thursday.plusWeeks(1);
}
// 计算时间差,即延时执行时间
long initialDelay = Duration.between(now, thursday).toMillis();
// 计算间隔时间,即 1 周的毫秒值
long oneWeek = 7 * 24 * 3600 * 1000;
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
System.out.println("开始时间:" + new Date());
executor.scheduleAtFixedRate(() -> {
System.out.println("执行时间:" + new Date());
}, initialDelay, oneWeek, TimeUnit.MILLISECONDS);
10) Tomcat スレッド プール
Tomcat はスレッド プールをどこで使用しますか?
- LimitLatch は電流を制限するために使用され、後で説明する JUC のセマフォと同様に、接続の最大数を制御できます。
- アクセプターは[新しいソケット接続を受信する]ことのみを担当します。
- ポーラーはソケット チャネルの [読み取り可能な I/O イベント] を監視することのみを担当します。
- 読み取り可能になったら、タスク オブジェクト (socketProcessor) をカプセル化し、処理のために Executor スレッド プールに送信します。
- Executor スレッド プール内のワーカー スレッドは、最終的に [リクエストの処理] を担当します。
Tomcat スレッド プールは、わずかに異なる動作で ThreadPoolExecutor を拡張します。
- スレッドの総数がmaximumPoolSizeに達した場合
- 現時点では、RejectedExecutionException はすぐにはスローされません。
- 代わりに、タスクを再度キューに入れてみて、失敗した場合は RejectedExecutionException をスローします。
ソースコード tomcat-7.0.42
public void execute(Runnable command, long timeout, TimeUnit unit) {
submittedCount.incrementAndGet();
try {
super.execute(command);
} catch (RejectedExecutionException rx) {
if (super.getQueue() instanceof TaskQueue) {
final TaskQueue queue = (TaskQueue)super.getQueue();
try {
if (!queue.force(command, timeout, unit)) {
submittedCount.decrementAndGet();
throw new RejectedExecutionException("Queue capacity is full.");
}
} catch (InterruptedException x) {
submittedCount.decrementAndGet();
Thread.interrupted();
throw new RejectedExecutionException(x);
}
} else {
submittedCount.decrementAndGet();
throw rx;
}
}
}
TaskQueue.java
public boolean force(Runnable o, long timeout, TimeUnit unit) throws InterruptedException {
if ( parent.isShutdown() )
throw new RejectedExecutionException(
"Executor not running, can't force a command into the queue"
);
return super.offer(o,timeout,unit); //forces the item onto the queue, to be used if the task
is rejected
}
- コネクタ構成
構成アイテム | デフォルト | 説明する |
---|---|---|
acceptorThreadCount |
1 | (接続を確立するための) アクセプタ スレッドの数 |
pollerThreadCount |
1 | ポーラースレッド数(多重監視チャネル) |
minSpareThreads |
10 | コアスレッドの数、つまり corePoolSize |
maxThreads |
200 | スレッドの最大数、つまり、maximumPoolSize |
executor |
- | 実行者名。次の実行者を参照するために使用されます。 |
- エグゼキュータスレッドの構成
構成アイテム | デフォルト | 説明する |
---|---|---|
threadPriority |
5 | スレッドの優先順位 |
daemon |
真実 | スレッドをガードするかどうか |
minSpareThreads |
25 | コアスレッドの数、つまり corePoolSize |
maxThreads |
200 | スレッドの最大数、つまり、maximumPoolSize |
maxIdleTime |
60000 | スレッドの存続時間 (ミリ秒単位)、デフォルト値は 1 分です |
maxQueueSize |
Integer.MAX_VALUE |
キューの長さ |
prestartminSpareThreads |
間違い | サーバー起動時にコアスレッドが開始されるかどうか |
この記事の参考文献:ダークホース プログラマーが Java 並行プログラミングを詳しく学ぶ、JUC 並行プログラミングのチュートリアルのフルセット