Java 並行プログラミングの基礎#
『並行プログラミングの芸術』読書ノート第 4 章、重点はスレッド間の通信
一、スレッドの紹介#
まずスレッドについて簡単に紹介します。
マルチスレッドを使用する理由:
1. より多くのプロセッサコア:1つのスレッドは同時に1つのプロセッサコアでしか実行できません。
2. より早い応答時間
3. より良いプログラミングモデル
JAVA プログラムの実行に必要なスレッド(jdk1.8)
スレッドの優先度
オペレーティングシステムは基本的にタイムシェアリングの形式で実行されるスレッドをスケジュールし、オペレーティングシステムは時間スライスを分け、スレッドは複数の時間スライスに割り当てられます。スレッドの時間スライスが使い果たされるとスレッドスケジューリングが発生し、次の割り当てを待ちます。スレッドに割り当てられる時間スライスの数が、スレッドがプロセッサリソースを使用する量を決定します。
一部のオペレーティングシステムはスレッドの優先度の設定を無視します。
Java スレッドでは、整数型メンバー変数priority
を使用して優先度を制御します。優先度の範囲は1~10
で、スレッドを構築する際にsetPriority(int)
メソッドを使用して優先度を変更できます。デフォルトの優先度は5
で、優先度の高いスレッドは優先度の低いスレッドよりも多くの時間スライスが割り当てられます。
スレッドの状態
6
種類あり、特定の時刻に 1 つの状態のみを持つことができます。
NEW
:初期状態、スレッドが構築されたが、まだ start メソッドが呼び出されていない。RUNNABLE
:実行状態、Java スレッドは == 準備完了と実行 == の 2 つの状態を総称して実行状態と呼びます。BLOCKED
:ブロック状態、スレッドがロックでブロックされていることを示します。WAITING
:待機状態、他のスレッドからの通知または中断を待っています。TIME_WAITING
:タイムアウト待機状態、指定された時間内に自動的に戻ることができます。TERMINATED
:終了状態、現在のスレッドが実行を完了したことを示します。
スレッド状態の遷移
Java はオペレーティングシステムの実行状態と準備完了状態を統合して実行状態と呼びます。
ブロック状態は、スレッドが
synchronized
キーワードで修飾されたメソッドまたはコードブロック(ロックを取得)に入るときの状態です。しかし、
java.concurrent
パッケージの Lock インターフェースのスレッド状態は待機状態であり、java.concurrent
パッケージの Lock インターフェースはブロックの実装にLockSupport
クラスの関連メソッドを使用しています。
デーモンスレッド(守護スレッド)
プログラムが実行されているときにバックグラウンドで一般的なサービスを提供するスレッド、例えばガベージコレクションスレッドです。プログラムに不可欠な部分ではありません。
Java 仮想マシンに非デーモンスレッドが存在しない場合、仮想マシンは終了します。Thread.setDaemon(true)
を使用してスレッドをデーモンスレッドに設定できますが、スレッドを起動する前に設定する必要があります。
ps. デーモンスレッドを構築する際、finally ブロック内の内容に依存してリソースのクリーンアップや閉鎖のロジックを確実に実行することはできません。実行されない可能性があります。
二、スレッドの起動と終了#
新しく構築されたスレッドオブジェクトは == 親スレッド == によってメモリが割り当てられ、子スレッドのさまざまな属性 == は親スレッドから継承され、同時に一意の ID が割り当てられます。==
Thread.init()のソースコードを確認
jdk1.8のソースコードにはThreadGroupに関する多くの判断操作が追加されています。
スレッドの安全性と例外状況のより良い区別が行われたと推測されます。
** スレッドの起動:**start メソッド:現在のスレッドが仮想マシンに通知し、スレッドスケジューラが空いている限り、start メソッドを呼び出したスレッドを直ちに起動する必要があります。== スレッドを作成する前にスレッド名を設定することが望ましく、エラーのトラブルシューティングに役立ちます。==
中断:
スレッドの識別子属性として理解できます。これは、実行中のスレッドが他のスレッドによって中断操作を受けたかどうかを示します。スレッド間の簡便な相互作用の方法です。
スレッドはisInterrupted()
メソッドを使用して中断されたかどうかを判断し、静的メソッドThread.interrupted()
を呼び出して現在のスレッドの中断フラグをリセットできます。
JavaのAPIからわかるように、多くのInterruptedExceptionをスローするメソッドは、この例外をスローする前に、Java仮想マシンがそのスレッドの中断フラグをクリアし、その後に例外をスローします。この時、isInterruptedを呼び出すとfalseが返されます。
- スレッドが待機状態またはタイムアウト待機状態(
TIMED_WAITING
、WAITING
)にあるとき、スレッドのinterrupt()
メソッドを呼び出すことでスレッドの待機を中断できます。この時、スレッドはInterruptedException
例外をスローします。 - しかし、スレッドが
BLOCKED
状態またはRUNNABLE
(RUNNING
)状態にあるとき、スレッドのinterrupt()
メソッドを呼び出しても、スレッドの状態フラグをtrue
に設定することしかできません。スレッドを停止するロジックは自分で実装する必要があります。
廃止された API:suspend()
,resume()
,stop()
理由:リソースを占有しているときにデッドロック問題が発生しやすく、スレッドが不確定な状態で共に動作することになります。
スレッドを終了させる方法:1 つの方法はinterrupt()
メソッドを使用するか、boolean 変数を利用してスレッドを終了させ、スレッドが終了時にリソースをクリーンアップする機会を持たせることです。
三、スレッド間通信(重点)#
1、volatile と synchronized キーワード#
volatile: プログラムに対して、この変数へのアクセスはすべて共有メモリから取得する必要があり、その変更は主メモリに同期してフラッシュされる必要があることを通知します。これにより、すべてのスレッドが変数へのアクセスの可視性を保証します。
synchronized:複数のスレッドが同時にメソッドまたは同期ブロックに入ることができないようにし、スレッドが変数にアクセスする可視性と排他性を保証します。再配置を保証することはできません。
synchronized
について:本質的にはオブジェクトのモニター(monitor
)の取得であり、この取得プロセスは排他であり、同時に 1 つのスレッドだけがsynchronized
で保護されたオブジェクトのモニターを取得できます。任意のオブジェクトは自分自身のモニターを持っています。
任意のスレッドが Object にアクセスするには、まず Object のモニターを取得する必要があります。取得に失敗すると、スレッドは同期キューに入ります。スレッドの状態は
BLOCKED
に変わります。Object の前駆(ロックを取得したスレッド)がロックを解放すると、その解放操作が同期キューでブロックされているスレッドを起こし、モニターの取得を再試行させます。
2、待機 / 通知メカニズム(生産者 - 消費者モデル)#
待機 / 通知メカニズムとは、スレッド A がオブジェクト O の
wait
メソッドを呼び出して待機状態に入り、別のスレッド B がオブジェクト O のnotify
またはnotifyAll
メソッドを呼び出すことを指します。スレッド A は通知を受け取った後、オブジェクト O のwait
メソッドから戻り、その後の操作を実行します。
ここでのコード例は無効です。waitスレッドが正常に起こされず、再配置も発生していません。
理由は、取得したロックが同じロックではないため、正常にロックを解除および取得できないことです。同期オブジェクトをpublic staticに設定して、同じオブジェクトを取得できるようにする必要があります。
///例はこの問題ではなく、最初にwaitがなかったためです...、😂
注意:
(1)wait
、notify
、notifyAll
を使用する際は、呼び出しオブジェクトにロックをかける必要があります。
(2)wait メソッドを呼び出した後、スレッドの状態はRUNNING
からWAITING
に変わり、現在のスレッドはオブジェクトの待機キューに移動します。
(3)notify
またはnotifyAll
メソッドが呼び出された後、待機スレッドは依然としてwait
から戻ることはありません。notify
またはnotifyAll
を呼び出したスレッドがロックを解放した後、待機スレッドはwait
から戻る機会を得ます。
(4)notify
メソッドは待機キュー内の待機スレッドを待機キューから同期キューに移動させ、移動されたスレッドはWAITING
からBLOCKED
に変わります。
(5)wait
メソッドから戻る前提は、呼び出しオブジェクトのロックを取得することです。
3、待機 / 通知の古典的なパターン#
待機側:1.オブジェクトのロックを取得
2.条件が満たされない場合、オブジェクトのwaitメソッドを呼び出し、通知を受けた後も条件を再確認
3.条件が満たされれば、対応するロジックを実行
擬似コード:
synchronized(オブジェクト){
while(条件が満たされない){
オブジェクト.wait();
}
対応する処理ロジック
}
通知側:1.オブジェクトのロックを取得
2.条件を変更
3.オブジェクト上のすべての待機スレッドに通知
擬似コード:
synchronized(オブジェクト){
条件を変更
オブジェクト.notify();
}
4、パイプ入力出力ストリーム#
パイプ入力 / 出力ストリームは、通常のファイル入力 / 出力ストリームやネットワーク入力 / 出力ストリームとは異なり、スレッド間のデータ転送に主に使用され、転送の媒体はメモリです。
PipedOutputStream
、PipedInputStream
(バイトデータ向け)
PipedReader
、PipedWriter
(文字向け)
piped
タイプのストリームを使用する際は、まずconnect
メソッドを呼び出してバインドする必要があります。そうしないと例外がスローされます。
パイプ入力バッファのサイズはデフォルトで 1024 バイトです。
書き込みサンプルコードの注意事項:
1. writeで書き込み、readで読み出す
2. 出力時はSystem.out.printを使用し、System.out.printlnではなく、後者は最後に改行を追加します。
5、Thread.join()#
意味:現在のスレッドは、thread スレッドが終了するのを待ってからthread.join
から戻ります。
さらに、join(long millis)
およびjoin(long millis,int nanos)
には == タイムアウト特性 == を持つメソッドがあります(指定された時間内にスレッドが終了しない場合、そのタイムアウトメソッドから戻ります)。
join()はwait(0)に相当し、指定されたスレッドのロックが解放されるのを待つだけでロックを奪取できます。
指定された時間後は、指定された時間後に再びロックを取得する必要があります(waitにパラメータを加えることは本質的にはnotify()メソッドを定時で呼び出すことと同じであり、このコードはJVM内で比較的低レベルです)。
joinにパラメータを加えると、最初はパラメータで設定されたwaitの覚醒時間を示し、その後はスレッドの活性をループで検出し、スレッドが常に生存している場合にのみ、設定値と現在の時間の差を渡し始めます。
6、ThreadLocal#
ThreadLocal、すなわちスレッド変数は、ThreadLocal オブジェクトをキー、任意のオブジェクトを値とするストレージ構造です。この構造はスレッドに付随しており、つまり、あるスレッドは ThreadLocal オブジェクトに基づいて、そのスレッドにバインドされた値を照会できます。
これはスレッドセーフなローカル変数です。
四、応用例#
一、待機タイムアウトモード#
メソッドを呼び出す際に一定の時間を待機します(一般的には指定された時間を与えます)。そのメソッドが指定された時間内に結果を得られた場合、結果を即座に返し、そうでなければタイムアウトしてデフォルトの結果を返します。
public synchronized Object get(long mills) throws InterruptedException {
long future = System.currentTimeMillis() + mills;
long remaining = mills;
// タイムアウトが0より大きく、resultの返り値が要件を満たさない場合
while ((result == null) && remaining > 0) {
wait(remaining);
remaining = future - System.currentTimeMillis();
}
return result;
}
典型的なケース:データベース接続プールパターン
ソースコード略
CountDownLatch:
- countDownLatch クラスは、あるスレッドが他のスレッドがそれぞれ実行を完了するのを待ってから実行されるようにします。
- これはカウンターを使用して実現され、カウンターの初期値はスレッドの数です。スレッドが実行を完了するたびにカウンターの値は - 1 され、カウンターの値が 0 になると、すべてのスレッドが実行を完了したことを示し、閉じ込められていたスレッドが作業を再開できます。
二、スレッドプール技術#
クライアントは execute (Job) メソッドを通じて Job をスレッドプールに提出し、クライアント自身は Job の実行が完了するのを待つ必要がありません。execute (Job) メソッドの他に、スレッドプールインターフェースはワーカー スレッドの増減やスレッドプールのシャットダウンメソッドを提供します。
ここでのワーカー スレッドは、Job を繰り返し実行するスレッドを表し、クライアントが提出した各 Job は、ワーカー スレッドの処理を待つ作業キューに入ります。
スレッドプールの本質は、スレッドセーフな作業キューを使用してワーカー スレッドとクライアント スレッドを接続することです。クライアント スレッドはタスクを作業キューに入れた後、戻り、ワーカー スレッドは作業キューから作業を取り出して実行し続けます。
典型的なケース:Web サーバーの実装
ソースコード略