blog

Javaスレッド割り込みの秘密

また、兄弟WHYから学び、各記事に不条理の段落を追加します。つまり、何気ない会話は、彼らの最近の生活の状態や感情、いくつかのアイデアや思考であってもよい、また、この記事などを書くの本来の意図であっても...

Aug 10, 2020 · 12 min. read
シェア

スレッド中断の不条理

また、各記事でとんでもない内容の段落を追加するために、なぜ兄から学びます。つまり、ランダムな話は、彼らの最近の生活の状態や感情、いくつかのアイデアや思考かもしれない、また、この記事を書くの本来の意図であるかもしれないなど、あるいは単にランダムな自慢話。

最近、おそらく仕事のプレッシャーのために少しので、常に非常に幸せではない感じ、数日前にも少し嘔吐する友人の輪の中で、多くの友人の励ましを参照してください、非常に感動しました。

実際、私は自分の20年について考えてみると、非常に幸運な、家族の調和、円滑な関係、教育もずさんなパスですが、仕事は数回ジャンプしているが、それも非常にスムーズです。車なし、家なし、子供なしの名前が、また良い生活、スーパーマーケットの自由や持っていますが。実際には、あまりにも多くの文句を言う必要はありませんが、あまりにも簡単ではないよりも、その世代の両親は、いわゆる "苦しみ "は何もありません。

大きな工場に入るのはあなたの選択ですから、がんばってください。

今朝は西湖に散歩に行き、夕方帰ってきて家族が作ってくれた大きな夕食を食べ、それから外に出て友達にスケボーを教えてもらい、新しい技をゲット。夜10時に帰ってきて、12時半近くまで書き続けていたので、たぶんハゲそうです。書けば書くほどコントロールできなくなるんだから。

"人生とは、それを愛そうとしなければ、本当に喜びを得ることはできないものなのです"

再び、私はこの記事を書きたい理由に戻って。実際には、金曜日、しばらくの間もつれたトピックの選択は、もともとSpring Iocを書きたいと思ったとき、その聴衆は少し広いかもしれませんが、ソースコードを見てかなり複雑ですが、気持ちはまた、それを見て背中をまっすぐに比較的大きな部分を費やすことです。読者や友人もメッセージを残すことができ、あなたは記事のどの側面を見たい、私にいくつかのインスピレーションを与えます。

このIDEAを中断スレッドはしばらく前にマルチスレッド電子書籍リーダーグループであり、パイナップルは、AQSについての質問に言及するように頼まれ、その後簡単にグループ内で議論し、いくつかの予備的な答えを思い付いた。

しかし、この先、私自身がスレッド割り込みのメカニズムを徹底的に整理したいと思い、マルチスレッドの本を書いたときには、この部分にはあまり焦点を当てませんでした。スレッド割り込みに関する記事は、実はネット上にも少ないので、この記事が参考になれば幸いです。

スレッド割り込みの使用シナリオ

スレッドの中断。スレッドを閉じたいという欲求のこと。

では、どのような場合にスレッド割り込みを使う必要があるのでしょうか?例えば、ドロップを取るとき、通常は複数のモデルを選ぶことができます。そして、あるモデルがヒットすれば、他のヒットはすべてキャンセルされます。

他の例としては、サードパーティのAPIをリクエストしに行き、制限時間内に結果が出ることを期待し、もし結果が出なければタスクをキャンセルしたくなるでしょう。

他のスレッドが行っていることに割り込む必要がある場合は、スレッド割り込みを使用することができます。

Javaのさまざまなスレッド・ユーティリティ・クラスも、スレッド割り込みメカニズムを多用します。

先制的中断と協調的中断

歴史的に、Javaはスレッドの終了にstop()メソッドを使用しており、それらはプリエンプティブ割り込み」に属していました。しかし、これは多くの問題を引き起こし、JDKでは長い間非推奨となっていました。stop()メソッドを使用すると、データの不整合が発生する可能性があり、スレッドをまったく停止しない可能性もあります。

" "

長い開発期間の後、Javaは最終的に協調的な割り込みメカニズムによる割り込みの実装を選択しました。

いわゆる協調アプローチは、割り込みマーカービットによって実装されます。他のスレッドがスレッド A に割り込みたい場合、スレッド A の割り込み識別子ビットをマークします。スレッド A 自身はポーリング」によって識別子ビットをチェックし、その後、自ら処理を行います。

そこで質問ですが、このサインビットはどこにあるのでしょうか?どのようにポーリングされるのでしょうか?スレッドが割り込みをポーリングするとき、スレッドは何をするのでしょうか?これらの質問については後で説明しますが、ここでは簡単に説明します。

Javaのスレッド割り込みの識別ビットはローカル・メソッド」によって維持され、ユーザーが取得して操作できるJavaレベルのAPIはわずかです。

ポーリングはどのように実施されますか?異なるシナリオは異なる方法で実装されます。スレッドのsleepやwaitのようなシナリオはJVM自身のポーリングによって実装されます。いくつかのJava同時実行ツールではポーリングはJavaコードレベルで実装されます。

スレッドが中断された後、スレッドは何をしますか?シナリオによって実装が異なり、多くの場合、ポーリング実装と組み合わせて実装されます。InterruptedException一般に、JVMを介してポーリングする実装は例外をスローしますが、Javaコード・レベルで実装されたポーリングは例外をスローしません。

スレッド割り込みのAPI

前述したように、Javaではスレッドの割り込み識別ビットはローカル・メソッドで管理されていますが、ユーザーが操作できるAPIがいくつか残されています。スレッド割り込みに関しては、Threadクラスがこれらのメソッドを定義しています:

// スレッドの中断
public void interrupt() {}
// スレッドが割り込みフラグをセットしているか確認する
public boolean isInterrupted() {}
// 現在のスレッドに割り込みフラグが設定されているかどうかを確認する。このメソッドは割り込みフラグをリセットし、副作用がある。
public static boolean interrupted() {}

JDKのソースコードを見ればわかりますが、これら3つのメソッドはネイティブ・メソッド呼び出しの実装の根底にあるもので、つまり、前述のように、割り込み識別ビットはネイティブ・メソッドによって維持され、Javaコード・レベルでは維持されません。

現在のスレッドが割り込みフラグをセットしたかどうかを判断するメソッドは2つあり、1つはisInterruptedで、よりシンプルで副作用がありません。もう1つは interrupted で、これは割り込みフラグをリセットし、副作用があります。

前置きはこれくらいにして、コードに移りましょう:

Thread.currentThread().interrupt();
System.out.println(Thread.interrupted());
System.out.println(Thread.interrupted());
System.out.println(Thread.interrupted());
// 以下を印刷する: true false false

1つは特定のスレッドが割り込みフラグをセットしているかどうかを確認するインスタンスメソッドで、もう1つは現在のスレッドが割り込みフラグをセットしているかどうかを確認するスタティックメソッドです。異なるメソッドは異なるアプリケーションのシナリオを持って、特に割り込み、その存在の意義は何ですか?次のソースコードを選んで、いくつかの応用シナリオを分析してみましょう。

割り込みのスレッド処理

Javaのスレッドには6つの状態があり、Thread.Stateクラスで定義されています。

// Thread.State  
public enum State {
 NEW,
 RUNNABLE,
 BLOCKED,
 WAITING,
 TIMED_WAITING,
 TERMINATED;
}
" "

"新規/終了"

スレッドがNEW状態またはTERMINATED状態の場合、interrupt()メソッドを呼び出しても何の影響もなく、割り込み識別ビットは設定されません。

"実行可能/ブロック"

スレッドが RUNNABLE または BLOCKED 状態の場合、interrupt() メソッドを呼び出すと割り込み識別ビットが設定されますが、スレッドの状態には影響しません。

「waiting/timed_waiting」。

スレッドはWAITING状態またはTIMED_WAITING状態にあり、便宜上「ブロッキング」と総称されます。ブロッキング状態では、JVMは割り込み識別子を積極的にポーリングします。

ブロッキング状態に入るには、一般的に2つの方法があります。 1つ目は、sleep、wait、joinなどのメソッドを使う方法です。InterruptedExceptionこれらのAPIはすべて明示的に例外をスローします:

// ウェイティングへ行く
public final void join() throws InterruptedException;
public final void wait() throws InterruptedException;
// TIMEDへ行く_WAITING
public final native void wait(long timeout) throws InterruptedException;
public static native void sleep(long millis) throws InterruptedException;
public final synchronized void join(long millis) throws InterruptedException;

上記のメソッドを呼び出してスレッドをブロッキング状態にした場合、interrupt()メソッドを呼び出すとスレッドがウェイクアップしてInterruptedExceptionがスローされることになります。

"

例外が発生すると、スレッドの割り込みフラグビットがtrueからfalseにリセットされるため、割り込みフラグビットはクリアされます。

"

上記のメソッドに加えて、スレッドをブロッキング状態にする別の方法があります、つまり、Javaのマルチスレッドツールクラスで広く使用されているLockSupport.parkメソッドです。

しかし、parkメソッドはsleep、wait、joinメソッドとは異なり、スレッド割り込みフラグがtrueに設定されている場合、ブロックせずに即座に戻り、例外をスローしないメソッドです。

IO割り込みの処理

"

BIOはInputStreamのような割り込みをサポートしておらず、割り込みスレッドは識別ビットを設定しますが、IOは割り込みを処理しません。

"

InterruptibleChannelClosedByInterruptException現在のスレッドがIO操作によってブロックされ、このチャネルがインタフェースを実装している場合、現在のスレッドの割り込みビットがtrueに設定され、チャネルがオフに切り替わり、スレッドは例外を受け取ります。

現在のスレッドがセレクタによってブロックされると、現在のスレッドの割り込みビットがtrueに設定され、セレクタは直ちに0以外の数値を返します。

ソースコード解析

では、JDKはスレッド割り込みをどのように使っているのでしょうか?身近なマルチスレッドツールのクラスを2つ選んでお話ししましょう。

AQS

まず第一に、有名なAQSは、多くのJavaマルチスレッドツールの基礎クラスとして、Javaマルチスレッド江湖で尊敬の位置にあります。そのコアメソッドacquireが最も重要です。呼び出しの基礎となるacquireメソッドはacquireQueuedメソッドですが、また、ここのコードのため、私はこの記事を書く衝動を持ってみましょう。

" "

まずは、このボールを見ても理解できないソースコードを楽しんでみましょう:

// エントランス、リソースを入手する
public final void acquire(int arg) {
 if (!tryAcquire(arg) &&
 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
 selfInterrupt();
}
// 自分自身を中断する
static void selfInterrupt() {
 Thread.currentThread().interrupt();
}
// リソースのキューイング
final boolean acquireQueued(final Node node, int arg) {
 boolean interrupted = false;
 try {
 for (;;) {
 final Node p = node.predecessor();
 if (p == head && tryAcquire(arg)) {
 setHead(node);
 p.next = null; // help GC
 return interrupted;
 }
 if (shouldParkAfterFailedAcquire(p, node))
 interrupted |= parkAndCheckInterrupt();
 }
 } catch (Throwable t) {
 cancelAcquire(node);
 if (interrupted)
 selfInterrupt();
 throw t;
 }
}
// park中断されたかどうかを検出する
private final boolean parkAndCheckInterrupt() {
 LockSupport.park(this);
 return Thread.interrupted();
}
"

追記:これはJava 11のソースコードで、Java 8のソースコードから若干の変更が加えられています。 |= の意味は +=, -= に似ています。

"

acquireQueuedの腰の部分のコードを見てみると、まず判断すべきことがあります:ロックの獲得に失敗した場合、パークすべきかどうか。もしそうなら、少しパークしてブロッキングに入り、interruptedをチェックします。割り込みがあれば、ここで割り込みをtrueにして、上記へ戻り、acquireメソッドの上で、selfInterruptメソッドを呼び出し、現在のスレッドに、割り込み識別ビットをセットします。

ここで少し考えてみてください:

「中断の後は

割り込み後、割り込まれたコードはtrueに設定されますが、次のループは続行されます。

  • 今回リソースがフェッチされた場合は、現在のスレッドの割り込み識別子ビットを設定し、上記へ返されます;
  • それでもまだリソースが得られない場合は、現在のスレッドをパークしてください。

"中断後すぐに戻って、ループを続ければいいじゃないですか"

現在のスレッドは、リソースを取得する試みに失敗したときに他のスレッドに割り込まれましたが、だからといってスレッドはすぐにすべての作業を中断して戻るべきではありません。いわゆる「協調的割り込み」とは、自分が安心して割り込める場所を選ぶべきだということです。

このメソッドでは、ループはリソースをフェッチするまで試行し続け、現在のスレッドがこのタスクを完了してから、外部に判断を委ねます。

簡単に言うと、中断されようがされまいが気にせず、自分のやっていることを終わらせて、中断されたことは自分で対処するということです。

"リソースを取得したら割り込み識別子ビットをtrueに設定したらどうですか?"

parkAndCheckInterruptの内部でThread.interruptedを呼び出すと、すでにfocusフラグビットがfalseにリセットされているため、現在のスレッドが中断されたという情報は外部で失われる」ので、ここでは、現在のスレッドが以前に中断されたことを外部に伝えることによって、この情報を補う必要があります。

"なぜacquireメソッドにはselfInterruptが必要なのですか?"

これはまだ割り込み情報の補正です。現在のスレッドは確かに割り込まれたので、自分では気にする必要はないのですが、やはり割り込みビットを戻して、外部にこの情報を認識させることが重要だからです。

"なぜここではisInterruptedメソッドではなくinterruptedメソッドが使われているのですか?"

なぜなら、parkAndCheckInterruptはループの中にあるからです。上述したように、もし今回リソースを取得できなければ、ループは継続されます。それでも取得できなかったら?ここでisInterruptedメソッドを使用すると、割り込み識別子ビットがリセットされないため、2回目にparkが現在のスレッドの割り込み識別子ビットがtrueに設定されていることを発見した場合、現在のスレッドをブロックすることなく、ただ後戻りすることになり、これは正しくありません。

簡単なテストコードです:

Thread thread = new Thread(() -> {
 LockSupport.park();
 System.out.println("first parked");
 Thread.currentThread().isInterrupted();
 LockSupport.park(); // 割り込みビットが真なので、ここではブロックされない
 System.out.println("second parked");
 Thread.interrupted();
 LockSupport.park(); // 割り込みビットが偽なので、ここでブロックされる
 System.out.println("third parked");
});
thread.start();
thread.interrupt(); // 中断、まず公園で目を覚ます
// プリントアウトする
// first parked
// third parked

Lock

Lockインターフェイスには1つのメソッドがあります:

void lockInterruptibly() throws InterruptedException;

実装は基本的にAQSのacquireSharedInterruptiblyメソッドの呼び出しで、独自の実装であるサブクラスがありますが、実装は似ています。Thread.interrupted()このメソッドは、現在のスレッドが割り込まれているかどうかをチェックし、識別子ビットをリセットするために使用されます。InterruptedExceptionすでに割り込まれている場合は、そのままスローします。

また、ご存知の通り、Lockインターフェイスのlockメソッドも、一度に1つのリソースを取得することを除けば、基本的にAQSを使って実装されています。

1つはlockメソッドで、割り込みに対して何もしません。もう1つはlockInterruptiblyメソッドで、割り込みに遭遇したときに例外をスローします。具体的にどのような使い方をするかは、ユーザが選択でき、非常に柔軟です。

ThreadPoolExecutor

また来ました、スレッドプール!

// 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();

簡単に言うと、スレッドプールが閉じている場合はスレッドが中断されるようにし、スレッドプールが閉じていない場合はスレッドが中断されないようにするということです。

スレッドプールの shutdown メソッドを見てください。このメソッドは、すべてのアイドル状態のワーカーに割り込むために interruptIdleWorkers メソッドを呼び出します。また、shutdownNow メソッドを見てください。このメソッドは、アイドル状態であるかどうかにかかわらず、すべてのワーカーに割り込むために interruptWorkers を呼び出します。

そのため、スレッドプールを優雅にシャットダウンしたい場合は、shutdownを呼び出す方がよいでしょう。

要約

このような大規模な分析の結果、いくつかの結論を導き出すことができます:

  • Javaは協調割り込みを使用しており、スレッド割り込みは識別子ビットを設定するだけです。
  • 割り込みはスレッドの状態によって扱いが異なり、ブロッキングは最も複雑です。
  • sleep、wait、joinなどは、JVMが識別子をポーリングして例外を投げます。
  • パークはJVMのポーリング識別子ビットですが、例外をスローしません。
  • nio割り込みに対応できること
  • ロックを使ったほうがいいし、割り込み例外を投げるかどうかは自分で決められます。

では、これからはスレッドブレイクを使うのですか?

私はヤシンです。顔が良くて楽しいプログラマーです。

個人ウェブサイト:https://yasinshaw.com

Read next

連鎖表形式での2つの数値の加算

2つの非負整数を表す2つの空でない連結リストが与えられます。ビットは逆順にソートされ、各ノードには1つの数値のみが格納されています。 先頭ノードを操作するには、ダミー・ノード dummy を作成し、 dummy->next を使用して真の先頭ノードを表すことを考えます。変数を使用して丸めを追跡し、最も低い有効ビットを含むテーブルから...

Aug 10, 2020 · 1 min read