序文
みなさん、こんにちは、jack xuです。これは並行プログラミングのパート2です。今日は、スレッドプールについてお話します。この記事は少し長く、少年たちは静かにそして根気よく彼を読みました。。
スレッドプールを使用する理由
1)スレッドの作成と破棄のパフォーマンスオーバーヘッドを削減する
2)応答速度の向上実行する新しいタスクがある場合、スレッドの作成を待たずにすぐに実行できます。
3)スレッドプールサイズを適切に設定すると、ハードウェアリソースのボトルネックを超えるスレッドの数によって引き起こされる問題を回避できます。
Alibabaのコード仕様を見てみましょう。プロジェクトでのスレッド作成は、スレッドプールを使用して作成する必要があります。その理由は、上記の3つの点を述べたからです。スレッドプールの使用
まず、UMLクラス図を見てみましょう
-
エグゼキューター:最上層がエグゼキューターインターフェイスであることがわかります。このインターフェースは非常に単純で、実行メソッドは1つだけです。このインターフェースの目的は、タスクの送信とタスクの実行を分離することです。
-
ExecutorService:これは引き続きExecutorから継承されたインターフェースであり、Executorインターフェースを拡張し、より多くのスレッドプール関連の操作を定義します。
-
AbstractExecutorService:ExecutorServiceのいくつかのデフォルト実装を提供します。
-
ThreadPoolExecutor:実際に使用するスレッドプールの実装はThreadPoolExecutorです。スレッドプール作業の完全なメカニズムを実装します。また、次の分析の焦点でもあります。
-
ForkJoinPool:ThreadPoolExecutorとThreadPoolExecutorの両方が、分割統治、再帰計算アルゴリズムに適したAbstractExecutorServiceから継承されます
-
ScheduledExecutorService:このインターフェイスは、ExecutorServiceを拡張して、タスクの遅延実行および定期実行のメソッドを定義します。
-
ScheduledThreadPoolExecutor:このインターフェイスは、ThreadPoolExecutorの継承に基づいてScheduledExecutorServiceインターフェイスを実装し、定期的および定期的にタスクを実行する特性を提供します。
上記の構造を理解することが重要です。エグゼキュータはツールクラスであり、スレッドを作成する2つの方法を検討します。1つ目は、エグゼキュータが提供するファクトリメソッドを使用してスレッドを実装する方法です。4つの方法があります。
Executor executor1 = Executors.newFixedThreadPool(10);
Executor executor2 = Executors.newSingleThreadExecutor();
Executor executor3 = Executors.newCachedThreadPool();
Executor executor4 = Executors.newScheduledThreadPool(10);
复制代码
二つ目は工法による
ExecutorService executor5 = new ThreadPoolExecutor(1,
1,
0L,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(2), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
复制代码
実際、最初の方法で作成されたソースコードを見ると、次のことがわかります。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
复制代码
基本的に、ThreadPoolExecutorのコンストラクターを呼び出すことにより、作成中にさまざまなパラメーターが渡されるため、本質的には、コンストラクターを使用するスレッドプールを作成する1つの方法しかありません。ここでは、Executorsのファクトリメソッドがどのように作成を支援したかについては説明しません。スレッドプールについて、別のAlibaba仕様を見てみましょう。
誰もがここで理解している、それはカプセル化が強すぎるためですが、作成された4つのスレッドプールに慣れていない限り、それを使用する方法がわからず、誤用、乱用がOOMにつながる可能性があります。使用していないので無駄に紹介しましたが、次に、ThreadPoolExecutor構築メソッドの各パラメーターの意味に注目します。構築メソッドは多数ありますが、最も完全なものを選択しました。public ThreadPoolExecutor(int corePoolSize, //核心线程数量
int maximumPoolSize, //最大线程数
long keepAliveTime, //超时时间,超出核心线程数量以外的线程空余存活时间
TimeUnit unit, //存活时间单位
BlockingQueue<Runnable> workQueue, //保存执行任务的队列
ThreadFactory threadFactory,//创建新线程使用的工厂
RejectedExecutionHandler handler //当任务无法执行的时候的处理方式)
复制代码
-
corePoolSize:スレッドプール内のコアスレッドの数、実際にはスレッドの最小数。allowCoreThreadTimeOutがない場合、コアスレッドの数内のスレッドは常に存続します。スレッドはそれ自体を破壊することはありませんが、一時停止状態のスレッドプールに戻ります。アプリケーションがスレッドプールに再度リクエストを送信するまで、スレッドプールの一時停止されたスレッドは実行タスクを再度アクティブにします。
-
maximumPoolSize:スレッドプール内のスレッドの最大数
-
keepAliveTimeと単位:コアスレッド数を超えた後の生存時間と単位
-
workQueue:は、スレッドプールによって実行されるすべてのタスクを保存するために使用されるブロッキングキューです。次の3つのタイプが一般に利用可能です。
1)ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
2)LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
3)SynchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
复制代码
-
ThreadFactory:通常、デフォルトのファクトリExecutors.defaultThreadFactory()を使用しますが、なぜファクトリを使用するのですか?実際には、生成されたスレッドを規制するためです。新しいスレッドの作成を呼び出さないでください。作成されたスレッドに違いが生じる可能性があります。
-
ハンドラー:キューと最大のスレッドプールがいっぱいになった後の飽和戦略。
1、AbortPolicy:直接抛出异常,默认策略;
2、CallerRunsPolicy:用调用者所在的线程来执行任务;
3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
4、DiscardPolicy:直接丢弃任务;
当然也可以根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略,如记录
日志或持久化存储不能处理的任务
复制代码
スレッドプールを作成した後も、対応するRunnableまたはCallableインターフェースの実装に対応して、戻り値あり、戻り値なしで非常に簡単に使用できます。
//无返回值
executor5.execute(() -> System.out.println("jack xushuaige"));
//带返回值
String message = executor5.submit(() -> { return "jack xushuaige"; }).get();
复制代码
ソースコード分析
メソッドを実行する
分析用のソースコードエントリに基づいて、最初に実行メソッドを確認します。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
复制代码
ソースコードに貼り付けていないキーコメントがあります。まず、このキーコメントの翻訳について説明しましょう。
これは3つのステップで処理されます。
1.実行中のスレッドの数がcorePoolSizeより少ない場合は、新しいスレッドを作成して、最初のタスクとして着信コマンドを実行してみてください。addWorkerを呼び出すと、runStateとworkCountが自動的にチェックされ、スレッドを追加してはならないときにスレッドを追加するというエラー警告が表示されなくなります。
2.タスクをキューに正常に追加できたとしても、スレッドを追加する必要があるか(最後のチェック後にスレッドが停止している可能性があるため)、またはこのメソッドを入力した後にスレッドプールが停止したかどうかを再度確認する必要があります。そのため、ステータスを再度確認し、必要に応じてキューをロールバックします。または、スレッドがない場合は、新しいスレッドを開始します。
3.タスクをキューに追加できない場合は、新しいスレッドを追加してみてください。追加が失敗した場合は、スレッドプールが閉じているか飽和しているため、タスクが拒否されます。
読んだ後もそれでも馬鹿げているようであれば、問題ありません。このフローチャートを下に書きます。
次に、ソースコードに含まれるctlを紹介します。ここをクリックしてソースコードを表示します
これは、スレッドの数とスレッドプールの状態を保存することを主な機能とするアトミッククラスであることがわかりました。彼はビット操作を使用しています。int値は32ビットです。ここで、上位3ビットは実行状態の保存に使用され、下位29ビットはスレッド数を節約します。ctlOf(RUNNING、0)メソッドを計算してみましょう。ここで、RUNNING = -1 << COUNT_BITS; -1は29ビットだけ左にシフトされ、-1のバイナリは32 1s(1111 1111 1111 1111 1111 1111 1111 1111)で、29だけ左にシフトされます。ビットが取得された後(1110 0000 0000 0000 0000 0000 0000 0000)、111 | 0または111は、同様に他の状態のビットビットを取得できます。このビット操作は非常に興味深いものです。ビットマップ操作はハッシュマップのソースコードでも使用されます。通常の開発ではGuysがビット操作を使用できるため、操作速度が速くなり、bを使用してインストールできます。これら5つのスレッドプールのステータスを紹介します
-
実行中:新しいタスクを受信し、キュー内のタスクを実行します
-
SHUTDOWN:新しいタスクを受け取りませんが、キュー内のタスクを実行します
-
STOP:新しいタスクを受信しない、キュー内のタスクを実行しない、進行中のタスクを中断する
-
TIDYING:すべてのタスクが終了し、スレッド数は0、この状態のスレッドプールは終了()メソッドを呼び出そうとしています
-
TERMINATED:Terminated()メソッドの実行が完了しました
それらの変換関係は次のとおりです。
addWorker方法
実行プロセスのコアメソッドがaddWorkerであることがわかります。分析を続けます。ソースコードはブラフのように見え、実際には2つのことを行い、それを分割しています。
最初のステップ:ワーカーの数を更新します。コードは次のとおりです。
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);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
复制代码
再試行は、ループと組み合わせて使用されるマークであり、再試行を続行すると、再試行の場所にジャンプして再実行されます。ブレイクリトライの場合は、ループボディ全体からジャンプします。ソースコードは最初にCTLを取得し、次にステータスを確認してから、作成されたスレッドのタイプに従って数量を確認します。CASを介してctlステータスを更新した後、成功すると、ループから抜けます。それ以外の場合は、スレッドプールの状態が再度取得され、元の状態と一致しない場合は、最初から実行されます。ステータスが変更されていない場合は、引き続きワーカー数を更新してください。フローチャートは次のとおりです。
ステップ2:ワーカーをワーカーセットに追加します。そして、ワーカーに保持されているスレッドを開始します。コードは次のとおりです。boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
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;
复制代码
作業を追加するときは、マルチスレッド同時実行の安全性を確保するために、最初にロックを取得する必要があることがわかります。ワーカーが正常に追加されると、ワーカー内のスレッドのstartメソッドが呼び出され、スレッドが開始されます。起動に失敗した場合は、addWorkerFailedメソッドを呼び出してロールバックします。この男を見ると、
1. ThreadPoolExecutorが起動せず、初期化後にスレッドが作成されません。executeメソッドが呼び出されると、 addWorkerが呼び出されてスレッドが作成されます。
2. addWorkerメソッドでは、新しいワーカーが作成され、そのワーカーによって保持されているスレッドがタスクを実行するために開始されます。
上記のように、スレッド数がcorePoolSizeに達した場合、コマンドのみがworkQueueに追加されます。そのため、コマンドはどのようにworkQueueに追加されて実行されますか?Workerのソースコードを分析してみましょう。
労働者階級
ワーカーはスレッドをカプセル化し、エグゼキューターでの作業の単位です。ワーカーはAbstractQueuedSynchronizerを継承し、Runnableを実装します。ワーカーの単純な理解は実際にはスレッドであり、runメソッドが再作成されています。彼の構築メソッドを見てみましょう。
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
复制代码
これら2つの重要な属性を見てみましょう
/** Thread this worker is running in. Null if factory fails. */
final Thread thread;
/** Initial task to run. Possibly null. */
Runnable firstTask;
复制代码
firstTaskは、それを使用して受信タスクを保存します。スレッドは、コンストラクターの呼び出し時にThreadFactoryによって作成されたスレッドであり、タスクの処理に使用されるスレッドです。ここでは、ThreadFactoryによって作成されたスレッドであり、直接新しいものはありません。その理由も上記に説明されていますこれがnewThreadによって渡されていることがわかります。Worker自体がRunnableインターフェースを継承しているため、addWorkで呼び出されたt.start()は、実際にはtが属するワーカーのrunメソッドを実行します。ワーカーのrunメソッドは次のとおりです。
public void run() {
runWorker(this);
}
复制代码
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 {
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);
}
}
复制代码
簡単な分析
1.最初にワーカーからfirstTaskを削除してクリアします。
2. firstTaskがない場合は、getTaskメソッドを呼び出して、workQueueからタスクを取得します。
3.ロックを取得します。
4. beforeExecuteを実行します。これは空のメソッドであり、必要に応じてサブクラスで実装されます。
5. task.runを実行します。
6. afterExecuteを実行します。これは空のメソッドであり、必要に応じてサブクラスで実装されます。
7.タスクをクリアし、タスク++を完了して、ロックを解除します。
8.例外がある場合、または実行可能なタスクがない場合は、外側のfinnalyコードブロックに入ります。現在のワーカーを終了するには、processWorkerExitを呼び出します。このワーカーを作業から削除した後、ワーカーの数がcorePoolSize未満の場合は、新しいワーカーを作成して、corePoolSizeスレッドの数を維持します。
この(while!= Null ||(task = getTask())!= Null)のコード行は、ワーカーがworkQueueからタスク実行を取得し続けることを保証します。getTaskメソッドはポーリングから取得されるか、BlockingQueue workQueueでタスクを取得します。
この時点で、executorがタスクを実行するスレッドを作成および開始するプロセスは明確に分析されており、shutdown()、shutdownNow()など、男の子が自分で観察および学習するためのその他のメソッドがあります。
スレッドプールのサイズを適切に構成する方法
スレッドプールのサイズは、推測に依存せず、多いほど良いという意味でもありません。
- CPUを集中的に使用するタスク:主に計算タスクを実行するために、応答時間が速く、CPUが実行されており、このタスクのCPU使用率が高いため、CPUコアの数に応じてスレッド数の構成を決定し、割り当てるスレッドを少なくする必要があります。 、CPU数に相当するサイズなど。
- IO集中型のタスク:主にIO操作に使用され、IO操作を実行する時間が長い。スレッドは常に実行されていないため、CPUはアイドル状態です。この場合、CPUの数など、スレッドプールのサイズを増やすことができます* 2
もちろん、これらはすべて経験値であり、実際の状況に応じてテストして最適な構成を取得するのが最善の方法です。
スレッドプールの監視
プロジェクトでスレッドプールが大規模に使用されている場合は、スレッドプールの現在の状態をガイドする監視システムを配置する必要があり、問題が発生したときに問題をすばやく見つけることができます。スレッドプールのbeforeExecute、afterExecute、shutdownメソッドを書き換えることで、スレッドの監視を実現できます。
これらの名前と定義から、これがサブクラスによって実装されており、スレッドの実行前、実行後、実行後にカスタムロジックを実行できることがわかります。まとめ
スレッドプールはシンプルで言うのが難しく、言うのも難しいです。使いやすいので簡単です。そのため、これについては何も言うことはないと考える人もいるかもしれません。困難なのは、基になるソースコードとスレッドのスケジュール方法を知ることです。はい、2つのことについて話しましょう。1つ目は、この記事では多くのフローチャートが使用されていることです。ソースコードを読んだり、複雑なビジネス開発を行ったりするときは、まず落ち着いて絵を描く必要があります。中断後、最初から最後まで側面を見ていかなければなりません。2番目はソースコードを読むことです。新卒のパートナーは、機能する限りそれを使用できますが、5年間働いている場合は、それを使用します。どのようにそれを達成するか、それから新卒者に対するあなたの利点は何ですか、そしてどの給与が新卒者より高いですか。さて、この記事の執筆者のレベルは限られています。質問がある場合は、共有して話し合ってください...