スレッドプール実行タスクの具体的なプロセスは何ですか?
ThreadPoolExecutor には、タスクを実行するための 2 つのメソッドが用意されています。
- voidexecute(実行可能なコマンド)
- Future<?> submit(実行可能なタスク)
実際、execute() メソッドは最終的に送信時に呼び出されますが、タスクの実行結果を取得するために Future オブジェクトを返します。
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
execute(Runnable command) メソッドは 3 つのステップで実行されます。
知らせ:
- Runnable を送信すると、現在のスレッド プール内のスレッドがアイドル状態であるかどうかに関係なく、その数がコア スレッドの数より少ない限り、新しいスレッドが作成されます。
- ThreadPoolExecutor は不公平に相当します。たとえば、キューがいっぱいになった後に送信された Runnable は、キューに入れられる Runnable より前に実行される可能性があります。
スレッド プールの 5 つの状態はどのように流れるのでしょうか?
スレッド プールには 5 つの状態があります
- 実行中: 新しいタスクが受信され、キュー内のタスクが処理されます。
- シャットダウン: 新しいタスクは受け入れられず、キュー内のタスクが処理されます。
- STOP: 新しいタスクは受信されず、キュー内のタスクは処理されず、処理中のタスクは中断されます (注: タスクを中断できるかどうかはタスク自体によって異なります)
- TIDYING: すべてのタスクが終了し、スレッド プールにスレッドが存在しないため、スレッド プールのステータスが TIDYING に変更され、このステータスに達すると、スレッド プールのterminated() が呼び出されます。
- TERMINATED:terminated() が実行されると、TERMINATED に変わります。
これら 5 つの状態を任意に変換することはできず、次の変換状況のみが存在します。
- RUNNING -> SHUTDOWN: shutdown() を手動で呼び出すことによってトリガーされるか、スレッド プール オブジェクトの GC 中に shutdown() を呼び出すために Finalize() が呼び出されます。
- (RUNNING または SHUTDOWN) -> STOP: shutdownNow() の呼び出しによってトリガーされます。shutdown() が最初に呼び出され、すぐに shutdownNow() が呼び出された場合、SHUTDOWN -> STOP が発生します。
- SHUTDOWN -> TIDYING: キューが空でスレッド プールにスレッドがない場合の自動変換
- STOP -> TIDYING: スレッド プールにスレッドがない場合に自動的に切り替えます (キューにタスクがある可能性があります)。
- TIDYING -> TERMINATED:terminated() の実行後に自動的に変換されます。
スレッド プール内のスレッドを閉じる方法
- 通常、スレッドを開始するには thread.start() メソッドを使用しますが、スレッドを停止するにはどうすればよいでしょうか?
- Thread クラスには stop() が用意されていますが、@Deprecated とマークされています。スレッドを停止するために stop() メソッドを使用することが推奨されないのはなぜですか?
- stop() メソッドは粗雑すぎるため、 stop() を呼び出すとスレッドは直接停止されますが、呼び出し時には、スレッドが何をしていたか、タスクがどのステップに到達したかがわかりません。危険な。
- ここで、stop() がスレッドによって占有されている同期ロックを解放することが強調されています(ReentrantLock ロックは自動的に解放されません。これが stop() が推奨されない要因でもあります)。
public class ThreadTest {
static int count = 0;
static final Object lock = new Object();
static final ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
public void run() {
// synchronized (lock) {
reentrantLock.lock();
for (int i = 0; i < 100; i++) {
count++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// }
reentrantLock.unlock();
}
});
thread.start();
Thread.sleep(5*1000);
thread.stop();
//
// Thread.sleep(5*1000);
reentrantLock.lock();
System.out.println(count);
reentrantLock.unlock();
// synchronized (lock) {
// System.out.println(count);
// }
}
}
したがって、変数をカスタマイズするか、次のように割り込みを行ってスレッドを停止することをお勧めします。
public class ThreadTest {
static int count = 0;
static boolean stop = false;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 100; i++) {
if (stop) {
break;
}
count++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
thread.start();
Thread.sleep(5 * 1000);
stop = true;
Thread.sleep(5 * 1000);
System.out.println(count);
}
}
違いは、stop を true に設定すると、スレッド自体が停止するかどうか、いつ停止するかを制御できることです。同様に、スレッドの中断() を呼び出してスレッドを中断できます。
public class ThreadTest {
static int count = 0;
static boolean stop = false;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 100; i++) {
if (Thread.currentThread().isInterrupted()) {
break;
}
count++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
}
}
});
thread.start();
Thread.sleep(5 * 1000);
thread.interrupt();
Thread.sleep(5 * 1000);
System.out.println(count);
}
}
違いは、スリープ中にスレッドが中断された場合、例外が受信されることです。
実際、interrupt() はスレッド。たとえば、shutdownNow() メソッドは
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
スレッド プールをブロッキング キューにする必要があるのはなぜですか?
プロセスの実行中、スレッド プール内のスレッドは、スレッドの作成時にバインドされた最初のタスクを実行した後、キューからタスクを取得し続け、キューにタスクがなくても、スレッドは自然に終了しません。キューのタスクを取得する際にはブロックされ、キューにタスクがあればタスクを取得して実行します。
このメソッドにより、最終的に指定された数のコア スレッドをスレッド プールに予約できるようになります。キー コードは次のとおりです。
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
スレッドがキューからタスクを取得するとき、タイムアウトブロッキングを使用してタスクを取得するかどうかを決定します。非コアスレッドはpoll()、コアスレッドはtake()、非コアスレッドはtake()を行うと考えることができます。時間が経過すると当然タスクの取得は失敗します。
スレッドに例外が発生した場合、その例外はスレッド プールから削除されますか?
答えは「はい」です。では、タスクの実行時にコア スレッドの数が間違っていて、すべてのコア スレッドがスレッド プールから削除される可能性はありますか?
- ソースコードでは、タスク実行時に例外が発生すると、最終的に processWorkerExit() が実行されますが、このメソッドの実行後、現在のスレッドは自然終了します。
- ただし、コア スレッドの固定数を維持できるように、processWorkerExit() メソッドに追加のスレッドが追加されます。
Tomcat がスレッド プールをカスタマイズする方法
Tomcat で使用されるスレッド プールは org.apache.tomcat.util.threads.ThreadPoolExecutor で、クラス名は JUC と同じですが、パッケージ名が異なります。
Tomcat は次のスレッド プールを作成します。
public void createExecutor() {
internalExecutor = true;
TaskQueue taskqueue = new TaskQueue();
TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
taskqueue.setParent( (ThreadPoolExecutor) executor);
}
受信キューは TaskQueue で、そのエンキュー ロジックは次のとおりです。
public boolean offer(Runnable o) {
//we can't do any checks
if (parent==null) {
return super.offer(o);
}
//we are maxed out on threads, simply queue the object
if (parent.getPoolSize() == parent.getMaximumPoolSize()) {
return super.offer(o);
}
//we have idle threads, just add it to the queue
if (parent.getSubmittedCount()<=(parent.getPoolSize())) {
return super.offer(o);
}
//if we have less threads than maximum force creation of a new thread
if (parent.getPoolSize()<parent.getMaximumPoolSize()) {
return false;
}
//if we reached here, we need to add it to the queue
return super.offer(o);
}
特別な分野:
- キューに参加するときは、スレッド プール内のスレッド数がスレッド プールの最大数と等しい場合にのみキューに参加します。
- キューに参加するときに、スレッド プール内のスレッド数がスレッド プールの最大数より少ない場合は、キューへの参加に失敗したことを示す false が返されます。
これは、タスクを送信するときに Tomcat のスレッド プールを制御します。
- やはり最初にスレッド数がコアスレッド数より少ないかどうかを判断し、コアスレッド数より少ない場合はスレッドを作成します。
- コアスレッド数と等しい場合はキューに参加しますが、スレッド数が最大スレッド数未満の場合はキューへの参加に失敗し、スレッドが作成されます。
したがって、タスクが送信されると、スレッドが最初に作成され、スレッド数が最大スレッド数と等しくなるまでキューに参加しません。
もちろん、比較的詳細なロジックがあります。タスクを送信するときに、処理中のタスクの数がスレッド プール内のスレッドの数よりも少ない場合、タスクはスレッドを作成せずに直接キューに追加されます。上記のソース コードの getSubmittedCount。
コアスレッドの数とスレッドプールの最大スレッド数を設定する方法
スレッド プールには 2 つの非常に重要なパラメータがあります。
- corePoolSize: コア スレッドの数。スレッド プール内の常駐スレッドの数を示します。
- MaximumPoolSize: スレッドの最大数。スレッド プール内で開くことができるスレッドの最大数を示します。
では、これら 2 つのパラメータを設定するにはどうすればよいでしょうか?
スレッド プールによって実行を担当するタスクは、次の 3 つの状況に分けられます。
- 1 から 1,000,000 までの素数を見つけるなど、CPU を大量に使用するタスク
- ファイル IO やネットワーク IO などの IO 集中型タスク
- 混合タスク
CPU 負荷の高いタスクの特性として、スレッドはタスクの実行時に常に CPU を使用するため、この場合、スレッドのコンテキストの切り替えは可能な限り回避する必要があります。
たとえば、私のコンピュータには現在 CPU が 1 つしかありません。2 つのスレッドが素数を見つけるタスクを同時に実行している場合、CPU はスレッド並列処理の効果を得るために追加のスレッド コンテキスト スイッチングを実行する必要があります。 2 つのスレッドが実行されている 合計時間は:
タスク実行時間2 + スレッド コンテキスト切り替え時間です
。スレッドが 1 つしかなく、このスレッドが 2 つのタスクを実行する場合、時間は次のようになります:
タスク 実行時間2 。スレッドの数は CPU コア番号と等しいのが最適です。次の API を通じてコンピューターのコア番号を取得できます。
Runtime.getRuntime().availableProcessors()
ただし、スレッド実行中のページ フォールトやその他の例外によって引き起こされるスレッド ブロッキング リクエストに応答するために、追加のスレッドをセットアップすることができます。これにより、スレッドが一時的に CPU を必要としないときに、代わりのスレッドを用意して継続的に使用できます。 CPUを活用します。
したがって、CPU を集中的に使用するタスクの場合、スレッド数を CPU コア数 + 1 に設定できます。
IO タイプのタスクを見てみましょう。スレッドが IO タイプのタスクを実行する場合、ほとんどの場合、IO でブロックされる可能性があります。現在 10 個の CPU がある場合、IO タイプのタスクを実行するために 10 個のスレッドのみをセットアップすると、非常に困難になります。おそらく、これら 10 個のスレッドが IO でブロックされているため、これら 10 個の CPU には作業がありません。したがって、IO タスクでは、通常、スレッド数を 2*CPU コア数に設定します。
ただし、2*CPU コア数に設定しても最適ではない可能性があり、たとえば、CPU が 10 個でスレッド数が 20 の場合、この 20 個のスレッドが IO でブロックされる可能性があります。同時にスレッドを追加できるため、CPU 使用率を抑えることができます。
通常、IO タイプのタスクの実行時間が長い場合、同時に IO でブロックされるスレッドが増える可能性があり、より多くのスレッドをセットアップできますが、スレッドが多いほど良いというわけではありません。
計算式:
スレッド数 = CPU コア数 * (1 + スレッド待機時間 / スレッドの合計実行時間)
- スレッド待機時間: IO のブロックなど、スレッドが CPU を使用していない時間を指します。
- スレッドの合計実行時間: スレッドがタスクを完了するのにかかる合計時間を指します。
スレッドプールの基本プロパティとメソッドのソースコード分析
スレッド プールのソース コードでは、AtomicInteger 型の変数 ctlを使用して、スレッド プールのステータスと現在のスレッド プール内の作業スレッドの数を表します。
整数は 4 バイト (32 ビット) を占有し、スレッド プールには 5 つの状態があります。
- 実行中: スレッド プールは正常に実行されており、タスクを正常に受け入れて処理できます。
- シャットダウン: スレッド プールは閉じられているため、新しいタスクを受け入れることができません。ただし、スレッド プールはブロッキング キュー内の残りのタスクを実行します。残りのタスクが処理された後、すべてのワーカー スレッドは中断されます。
- STOP: スレッド プールが停止しており、新しいタスクを受け入れることができず、ブロッキング キュー内のタスクを処理しないため、すべてのワーカー スレッドが中断されます。
- TIDYING: 現在のスレッド プール内のすべての作業スレッドが停止した後、TIDYING に入ります。
- TERMINATED: スレッド プールが TIDYING 状態になると、terminated() メソッドが実行されます。実行後、TERMINATED 状態になります。ThreadPoolExecutor では、terminated() は空のメソッドです。スレッド プールをカスタマイズしてオーバーライドできます。この方法。
2 ビットで 4 つの状態を表すことができ、これらの 5 つの状態には少なくとも 3 ビットが必要です。たとえば、スレッド プールのソース コードでは次のように表されます。
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
Integer.SIZE は 32 であるため、COUNT_BITS は 29 です。各状態に対応する最終的なセカンダリ システムは次のとおりです。
- 実行中:11100000 00000000 00000000 00000000
- シャットダウン:00000000 00000000 00000000 00000000
- ストップ:00100000 00000000 00000000 00000000
- 片付け:01000000 00000000 00000000 00000000
- 終了:01100000 00000000 00000000 00000000
したがって、5 つのスレッド プールのステータスを表すには、整数の上位 3 ビットのみを使用する必要があり、残りの 29 ビットは、作業スレッドの数を表すために使用できます。たとえば、ctl が次の場合: 11100000 00000000 00000000 00001010 , it means that the status of the thread pool is RUNNING, and there are 10 thread are currently working in the thread pool. ここでの「動作中」とは、スレッドが生きていて、タスクを実行しているか、タスクをブロックして待機していることを意味します。
同時に、スレッド プールは、スレッド プールのステータスと作業スレッドの数を取得するための次のようなメソッドも提供します。
// 29,二进制为00000000 00000000 00000000 00011101
private static final int COUNT_BITS = Integer.SIZE - 3;
// 00011111 11111111 11111111 11111111
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// ~CAPACITY为11100000 00000000 00000000 00000000
// &操作之后,得到就是c的高3位
private static int runStateOf(int c) {
return c & ~CAPACITY;
}
// CAPACITY为00011111 11111111 11111111 11111111
// &操作之后,得到的就是c的低29位
private static int workerCountOf(int c) {
return c & CAPACITY;
}
同時に、次のような別の方法もあります。
private static int ctlOf(int rs, int wc) {
return rs | wc;
}
実行状況とワーカースレッド数を組み合わせるメソッドですが、このメソッドに渡す2つのint値には制限があり、rsの下位29ビットは0、wcの上位3ビットは0でなければなりません, したがって、OR 演算の後、正確な ctl を取得できます。
同時に、関連するメソッドがいくつかあります
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
// c状态是否小于s状态,比如RUNNING小于SHUTDOWN
private static boolean runStateLessThan(int c, int s) {
return c < s;
}
// c状态是否大于等于s状态,比如STOP大于SHUTDOWN
private static boolean runStateAtLeast(int c, int s) {
return c >= s;
}
// c状态是不是RUNNING,只有RUNNING是小于SHUTDOWN的
private static boolean isRunning(int c) {
return c < SHUTDOWN;
}
// 通过cas来增加工作线程数量,直接对ctl进行加1
// 这个方法没考虑是否超过最大工作线程数的(2的29次方)限制,源码中在调用该方法之前会进行判断的
private boolean compareAndIncrementWorkerCount(int expect) {
return ctl.compareAndSet(expect, expect + 1);
}
// 通过cas来减少工作线程数量,直接对ctl进行减1
private boolean compareAndDecrementWorkerCount(int expect) {
return ctl.compareAndSet(expect, expect - 1);
}
メソッドを実行する
スレッドプールのexecuteメソッドを実行する場合:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
// 获取ctl
// ctl初始值是ctlOf(RUNNING, 0),表示线程池处于运行中,工作线程数为0
int c = ctl.get();
// 工作线程数小于corePoolSize,则添加工作线程,并把command作为该线程要执行的任务
if (workerCountOf(c) < corePoolSize) {
// true表示添加的是核心工作线程,具体一点就是,在addWorker内部会判断当前工作线程数是不是超过了corePoolSize
// 如果超过了则会添加失败,addWorker返回false,表示不能直接开启新的线程来执行任务,而是应该先入队
if (addWorker(command, true))
return;
// 如果添加核心工作线程失败,那就重新获取ctl,可能是线程池状态被其他线程修改了
// 也可能是其他线程也在向线程池提交任务,导致核心工作线程已经超过了corePoolSize
c = ctl.get();
}
// 线程池状态是否还是RUNNING,如果是就把任务添加到阻塞队列中
if (isRunning(c) && workQueue.offer(command)) {
// 在任务入队时,线程池的状态可能也会发生改变
// 再次检查线程池的状态,如果线程池不是RUNNING了,那就不能再接受任务了,就得把任务从队列中移除,并进行拒绝策略
// 如果线程池的状态没有发生改变,仍然是RUNNING,那就不需要把任务从队列中移除掉
// 不过,为了确保刚刚入队的任务有线程会去处理它,需要判断一下工作线程数,如果为0,那就添加一个非核心的工作线程
// 添加的这个线程没有自己的任务,目的就是从队列中获取任务来执行
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 如果线程池状态不是RUNNING,或者线程池状态是RUNNING但是队列满了,则去添加一个非核心工作线程
// 实际上,addWorker中会判断线程池状态如果不是RUNNING,是不会添加工作线程的
// false表示非核心工作线程,作用是,在addWorker内部会判断当前工作线程数已经超过了maximumPoolSize,如果超过了则会添加不成功,执行拒绝策略
else if (!addWorker(command, false))
reject(command);
}
addWorker メソッド
addWorker メソッドはコア メソッドであり、スレッドを追加するために使用されます。core パラメータは、コア スレッドを追加するか、非コア スレッドを追加するかを示します。
このメソッドを検討する前に、まず自分で分析してみるとよいでしょう。スレッドの追加とは何でしょうか?
実際には、スレッドを開始する必要があります。コア スレッドであっても、非コア スレッドであっても、実際には単なる通常のスレッドです。コアと非コアの違いは次のとおりです: コア ワーカー スレッドを追加したい
場合, 現在のワーカー スレッドの数を決定する必要があります。 corePoolSize を超えているかどうか
。超えていない場合は、タスクを実行するために新しいワーカー スレッドが直接開始されます。
超えている場合は、新しいワーカー スレッドは開始されませんが、タスクはキューに入れられます。
非コア ワーカー スレッドを追加する場合は、現在の状況を判断する必要があります。作業スレッドの数が MaximumPoolSize を超えているかどうか。超えていない場合は、
新しい作業スレッドが直接開始されてタスクが実行されます。
これを超えるとタスクの実行が拒否されるため、
addWorkerメソッドではまず作業スレッドが制限を超えているかどうかを判定し、制限を超えていなければスレッドを開始します。
そして、addWorker メソッドでは、スレッド プールのステータスを確認する必要があります。スレッド プールのステータスが RUNNING でない場合、スレッドを追加する必要はありません。もちろん、特殊なケースがあります。スレッド プールのステータスは SHUTDOWN ですが、キュー内にタスクがある場合は、この時点でもスレッドを追加する必要があります。
では、この特殊なケースはどのようにして生じたのでしょうか?
先ほど述べたのは、新しい作業スレッドを開始することですが、作業スレッドをリサイクルするにはどうすればよいでしょうか? 開かれた作業スレッドが常にアクティブであることは不可能です。タスクの数が多い状態から少ない状態に減少すると、あまり多くのスレッド リソースが必要なくなるため、開始されたスレッドを再利用するメカニズムがスレッド プール内に存在します。作業スレッドです。リサイクル方法については後で説明します。前述したように、まず、スレッド プール内のすべてのスレッドがリサイクルされた可能性があるかどうかを分析しましょう。答えは「はい」です。
まず、非コア ワーカー スレッドがリサイクルされることは理解できますが、コア ワーカー スレッドはリサイクルされるべきでしょうか? 実際、スレッド プールの存在の意味は、スレッド リソースを事前に生成しておき、スレッドが必要になったときに直接使用することであり、スレッドを一時的にオープンする必要はありません。一時的に利用できなくなった場合でも、スレッドをリサイクルする必要はありません。タスクを処理する必要がある場合は、コア ワーカー スレッドをそこで待機させておくだけです。
しかし!スレッド プールには、allowCoreThreadTimeOut というパラメータがあります。これは、コア ワーカー スレッドのタイムアウトが許可されるかどうか、つまりコア ワーカー スレッドのリサイクルが許可されるかどうかを示します。デフォルトのパラメータは false ですが、allowCoreThreadTimeOut(boolean) を呼び出すことができます。 value) を使用して、このパラメータを true に変更します。変更されている限り、コア ワーカー スレッドもリサイクルされるため、スレッド プール内のすべてのワーカー スレッドがリサイクルされる可能性があります。その後、すべてのワーカー スレッドがリサイクルされると、タスクががブロッキングキューに入ります。これにより特殊なケースが発生します。