序文
スレッド デッドロックはよくある問題です。スレッド プールのデッドロックは、本質的にスレッド デッドロックの一部です。スレッド プールによって引き起こされるデッドロックの問題は、多くの場合、ビジネス シナリオに関連しています。もちろん、より重要なことは、スレッド プールについての理解が不足していることです。この記事は次のとおりです。シナリオに基づいて、一般的なスレッド プールのデッドロックの問題について説明します (もちろん、スレッド デッドロックの問題も含まれます)。
スレッドデッドロックのシナリオ
デッドロックのシナリオは多数あり、スレッド プールに関連するものと、スレッドに関連するものがあります。スレッド関連のスレッド プールもよく表示されますが、必ずしもその逆であるとは限りません。この記事では、いくつかの一般的なシナリオを要約します。もちろん、いくつかのシナリオは補足する必要があるかもしれません。後で。
古典的な相互排他関係のデッドロック
この種のデッドロックは、最も一般的な古典的なデッドロックです。A と B という 2 つのタスクがあるとします。A は B のリソースを必要とし、B は A のリソースを必要とします。両方の当事者がリソースを取得できない場合、デッドロックが発生します。この場合、ロックは直接的な原因はお互いの待機によって発生し、通常はダンプヒープのロック ハッシュコードを通じて見つけることができ、比較的簡単に見つけることができます。
//首先我们先定义两个final的对象锁.可以看做是共有的资源.
final Object lockA = new Object();
final Object lockB = new Object();
//生产者A
class ProductThreadA implements Runnable{
@Override
public void run() {
//这里一定要让线程睡一会儿来模拟处理数据 ,要不然的话死锁的现象不会那么的明显.这里就是同步语句块里面,首先获得对象锁lockA,然后执行一些代码,随后我们需要对象锁lockB去执行另外一些代码.
synchronized (lockA){
//这里一个log日志
Log.e("CHAO","ThreadA lock lockA");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){
//这里一个log日志
Log.e("CHAO","ThreadA lock lockB");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
//生产者B
class ProductThreadB implements Runnable{
//我们生产的顺序真好好生产者A相反,我们首先需要对象锁lockB,然后需要对象锁lockA.
@Override
public void run() {
synchronized (lockB){
//这里一个log日志
Log.e("CHAO","ThreadB lock lockB");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockA){
//这里一个log日志
Log.e("CHAO","ThreadB lock lockA");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
//这里运行线程
ProductThreadA productThreadA = new ProductThreadA();
ProductThreadB productThreadB = new ProductThreadB();
Thread threadA = new Thread(productThreadA);
Thread threadB = new Thread(productThreadB);
threadA.start();
threadB.start();
このような問題には調査と継続的な最適化が必要であり、ロックの使用を最小限に抑えるためのロジックの最適化と、スケジューリング メカニズムの最適化に重点が置かれています。
再帰待機コールのデッドロックを送信する
原則として一定数のスレッドプールにタスクを投入し続け、ワーキングスレッドからのgetでタスクの完了を待つことになりますが、スレッドプール数は固定であり、最初から最後まですべてのスレッドが実行されない場合には、タスクの処理にはスレッドが使用され、すべてのタスクが待機状態になります。
ExecutorService pool = Executors.newSingleThreadExecutor(); //使用一个线程数模拟
pool.submit(() -> {
try {
log.info("First");
//上一个线程没有执行完,线程池没有线程来提交本次任务,会处于等待状态
pool.submit(() -> log.info("Second")).get();
log.info("Third");
} catch (InterruptedException | ExecutionException e) {
log.error("Error", e);
}
});
この特別なロジックでは、get メソッドの呼び出しの意味をよく考える必要がありますが、シリアル実行だけであれば一般的なキューを使用してください。もちろん、他のスレッドに参加することもできます。
パブリック スレッド プールのスレッド サイズ不足によって発生するデッドロック
このタイプのデッドロックでは、通常、複数のタスクに対してサイズが制限されたスレッド プールが使用されます。
2 つのビジネス A と B が、プロデューサー ビジネスとコンシューマー ビジネスを処理するためにそれぞれ 2 つのスレッドを必要とし、各ビジネスが独自のロックを持っているとしますが、ビジネス間のロックは関連していないとします。スレッド サイズ 2 のパブリック スレッド プールを提供します。明らかに、より合理的なタスク実行には 4 つ、または少なくとも 3 つが必要です。スレッド数が不足すると、高確率でデッドロックが発生します。
シナリオ 1: A と B はデッドロックを引き起こすことなく順番に実行されます。
シナリオ 2: A と B が同時に実行され、デッドロックが発生する
2 番目の状況の理由は、A と B にそれぞれスレッドが割り当てられていることです。実行条件が満たされていない場合、それらは待機状態になります。この時点で、スレッド プールには提供できるスレッドがこれ以上ありません。 AとBは行き詰まります。
したがって、パブリック スレッド プールを使用する場合は、サイズを低く設定しすぎず、ロックや時間のかかるタスクはできるだけ避ける必要があります。専用のスレッド プールの使用を試みることができます。
RejectedExecutionHandler の不適切な使用によって発生する「デッドロック」
厳密に言えば、行き詰まりとは言えませんが、これは非常に無視されやすい問題でもあります。その理由は、スレッド プールのステータスが検出されずに、RejectionExecutionHandler コールバック メソッドなどを通じてタスクが再度追加され、Caller スレッドがロックされるためです。
一般にタスクを処理する場合、RecjectedExecutionHandler が発生する状況は主に「スレッドプールがクローズされた」場合と「スレッドキューおよびスレッド数が最大容量に達した場合」の 2 つに分類され、一般的に問題は前者で発生します。スレッド プールがシャットダウンした場合、このハンドラーでスレッド プールにタスクを再追加しようとすると、無限ループの問題が発生します。
無限ループをロックする
無限ループのロック自体もデッドロックとなり、ロック リソースを取得したい他のスレッドが正常に割り込みを取得できなくなります。
synchronized(lock){
while(true){
// do some slow things
}
}
この種のループ ロックも非常に古典的で、while 内で wait、return、break の呼び出しがない場合、このロックは常に存在します。
ファイルロックとミューテックスロック
厳密に言うと、これは比較的複雑で、ファイル ロックとロックが相互に排他的である可能性や、マルチプロセス ファイル ロックがブロックされて取得後に解放できず、その結果 Java ロックが解除できなくなる可能性があります。したがって、デッドロックが発生した場合は、ファイル操作に関連するスタックをダンプする際にデッドロックを無視しないでください。
十分な視認性が得られない
通常、これはデッドロックではなく、無限スレッド ループであるため、そのスレッドは他のタスクで使用できなくなります。いくつかのスレッド ループに変数を追加して、終了したかどうかをマークしますが、可視性が不十分な場合は、スレッド ループが終了しません。終了の原因とその結果。
以下ではメインスレッドと通常スレッドを使ってシミュレーションを行っていますが、通常スレッドで変数Aを変更していますが、メインスレッドでは変数Aの可視性が不十分で、メインスレッドがブロックされてしまいます。
public class ThreadWatcher {
public int A = 0;
public static void main(String[] args) {
final ThreadWatcher threadWatcher = new ThreadWatcher();
WorkThread t = new WorkThread(threadWatcher);
t.start();
while (true) {
if (threadWatcher.A == 1) {
System.out.println("Main Thread exit");
break;
}
}
}
}
class WorkThread extends Thread {
private ThreadWatcher threadWatcher;
public WorkThread(ThreadWatcher threadWatcher) {
super();
this.threadWatcher = threadWatcher;
}
@Override
public void run() {
super.run();
System.out.println("sleep 1000");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.threadWatcher.A = 1;
System.out.println("WorkThread exit");
}
}
結果を出力します。
sleep 1000
WorkThread exit
A が見えないためメインスレッドがループし続けるため、volatile を追加するかアトミッククラスを使用するか、同期には synchronized を使用する必要があります。Final は命令の順序が狂っていないことを保証するだけで、可視性を保証することはできません。
CountDownLatch の初期値が大きすぎます
この理由はプログラミング上の問題で、たとえば、待機を完了するには countDown を 2 回完了する必要があり、初期値が 3 回を超えると、待機中のスレッドが必然的にスタックしてしまいます。
CountDownLatch latch = new CountDownLatch(6);
ExecutorService service = Executors.newFixedThreadPool(5);
for(int i=0;i< 5;i++){
final int no = i+1;
Runnable runnable=new Runnable(){
@Override
public void run(){
try{
Thread.sleep((long)(Math.random()*10000));
System.out.println("No."+no+"准备好了。");
}catch(InterruptedException e){
e.printStackTrace();
}finally{
latch.countDown();
}
}
};
service.submit(runnable);
}
System.out.println("开始执行.....");
latch.await();
System.out.println("停止执行");
実際、この種の問題はトラブルシューティングが比較的簡単です。ウェイターをカウントする場合は、異常な動作が発生した場合でもウェイターを終了できることを確認してください。
スレッドデッドロック最適化の提案
デッドロックは一般にブロッキングに関連しているため、デッドロックの問題に対処するには、別の方法を試したほうがよいでしょう。
一般的な最適化手法
- 1. 順番に実行できますが、当然、これにより同時実行性の利点も減ります。
- 2. 同じスレッド プールを共有しないでください。共有する場合は、ロック、ブロック、ハングを避けてください。
- 3. パブリック ロック リソースの待機 (ロング タイムアウト) メカニズムを使用して、スレッドがタイムアウトになるようにします。
- 4. スレッド プールがリサイクルできないことを心配する場合は、keepaliveTime+allowCoreThreadTimeOut を使用してスレッドをリサイクルすることをお勧めしますが、スレッドのステータスには影響を与えず、タスクの送信を続行できます。
- 5. 必要に応じてスレッド プール サイズを拡張します。
パブリックスレッドタスクの削除
パブリック スレッド プールによって実行されているスレッドがブロックされている場合、すべてのタスクは待機する必要がありますが、重要でないタスクについては削除することを選択できます。
実際、実行中のスレッド タスクを終了するのは難しく、パブリック スレッド プールによって大量の保留タスクが発生する可能性がありますが、パブリック スレッド プールからタスク キューを削除することは明らかにより危険な操作です。考えられる方法の 1 つは、タスクをワープし、実行可能ファイルが追加されるたびにこれらのタスクを記録し、特定のビジネスを終了するときにワーッパー内のターゲット タスクをクリーンアップすることです。
public class RemovableTask implements Runnable {
private static final String TAG = "RemovableTask";
private Runnable target = null;
private Object lock = new Object();
public RemovableTask(Runnable task) {
this.target = task;
}
public static RemovableTask warp(Runnable r) {
return new RemovableTask(r);
}
@Override
public void run() {
Runnable task;
synchronized (this.lock) {
task = this.target;
}
if (task == null) {
MLog.d(TAG,"-cancel task-");
return;
}
task.run();
}
public void dontRunIfPending() {
synchronized (this.lock) {
this.target = null;
}
}
}
以下のタスクをクリーンアップします
public void purgHotSongRunnable() {
for (RemovableTask r : pendingTaskLists){
r.dontRunIfPending();
}
}
RemovableTask の作成を減らすために、ここでもフライウェイト モードの最適化を使用できることに注意してください。
多重化またはコルーチンを使用する
ロックを嫌う開発者は、多重化やコルーチンを利用することで、不要な待機を回避し、待機を通知に変換し、コンテキストスイッチを減らし、スレッドの実行効率を向上させることができます。
コルーチンの見方に関しては、常に次のような議論があります。
(1) コルーチンは軽量スレッドですか? しかし、CPU とシステムの観点から見ると、コルーチンとマルチプレクサは軽量のスレッドではありません。CPU はそれらをまったく認識しないため、スレッドより高速になることはできません。スレッドの実行を加速することしかできません。Okhttp は軽量スレッドではありません。 Socket のいずれかです。どんなに高速であっても、Socket よりも高速であることはできません。これらはすべて同時プログラミングのフレームワークまたはスタイルです。
(2) Kotlin は偽のコルーチンではない kotlin はスレッドを生成するので偽のコルーチンではないかと言う人もいます。epoll 多重化メカニズム、すべてのタスクは epoll によって実行されますか? 簡単な例としては、ディスクからメモリへのファイルのコピーが挙げられます。CPU は関与しませんが、DMA もチップであり、間違いなくスレッドと見なされます。コルーチンはユーザー モードで時間のかかるタスクを実行します。スレッドが有効になっていない場合、無数のエントリ ポイントを挿入して単一のスレッドでタスクを実行できるようにすることは可能でしょうか? コルーチンの理解を賞賛する人も批判する人も当然いますが、その主な理由は、「フレームワーク」と実行ユニットに認知的な問題があることです。
ロックの粒度を減らす
JIT のロックの最適化はロックの削除とロックの再入に分けられますが、ロックの粒度の最適化は難しいため、あまり大きなコードセグメントを追加しない必要があるのは明らかです。ロックする必要はなく、変数を変更する部分をロックするだけです。
要約する
この記事は主にデッドロックの問題に対する最適化の提案についてです。パフォーマンスの問題に関しては、実際には、スムーズさを確保しながらスレッドが少ないほど良いという原則に従っています。必要なスレッドについては、キューのバッファリング、エスケープ分析、オブジェクトのスカラー化、ロックの削除、ロックの粗密化、ロック範囲の縮小、多重化、同期バリアの削除、コルーチンのパースペクティブを使用して最適化できます。
やっと
アーキテクトになりたい場合、または 20,000 ~ 30,000 の給与範囲を突破したい場合は、コーディングとビジネスに限定されず、選択して拡張し、プログラミング的思考を向上させることができなければなりません。また、適切なキャリアプランニングも非常に重要で、学習習慣も重要ですが、最も重要なのは忍耐力であり、一貫して実行できない計画は空虚です。
方向性がわからない場合は、Alibaba の上級アーキテクトによって書かれた一連の「Android の 8 つのモジュールに関する上級メモ」を参照してください。これは、乱雑で分散し断片化した知識を体系的に整理するのに役立ち、さまざまなモジュールを体系的かつ効率的にマスターできるようにするのに役立ちます。 Android開発の知識ポイント。
私たちが普段読んでいる断片的な内容と比べて、このノートの知識ポイントはより体系的で理解しやすく記憶しやすく、知識体系に従って厳密に整理されています。
ワンクリックと 3 つのリンクで皆さんのサポートを歓迎します。記事内の情報が必要な場合は、記事の最後にある CSDN 公式認定 WeChat カードをスキャンするだけで無料で入手できます↓↓↓ (ちょっとした特典もあります)記事の最後にある ChatGPT ロボット、お見逃しなく)