2024 Java インタビュー -- マルチスレッド (3)

シリーズ記事の目次

  1. 2024 Java インタビュー (1) – 春の章
  2. 2024 Java インタビュー (2) – 春の章
  3. 2024 Java インタビュー (3) – 春の章
  4. 2024 Java インタビュー (4) – 春の章
  5. 2024 Java インタビュー – コレクション
  6. 2024 年の Java インタビュー – redis(1)
  7. 2024 年の Java インタビュー – redis(2)


スレッドの安全性

1. デッドロックの原因

1. システムリソースが不足しているため

2. プロセスの実行順序と進行順序が不適切です。

3. リソースの不適切な割り当て

システムリソースが十分にあり、プロセスのリソース要求を満たせる場合にはデッドロックが発生する可能性は非常に低くなりますが、そうでない場合は、限られたリソースの奪い合いによりデッドロックに陥ってしまいます。次に、プロセスの実行順序と速度が異なる場合にもデッドロックが発生する可能性があります。


2. デッドロックが発生する4つの条件

1. 相互排他条件: リソースは一度に 1 つのプロセスのみが使用できます。

2. 要求と保持の条件: リソースの要求によりプロセスがブロックされた場合、プロセスは取得したリソースを保持します。

3. 剥奪不可能条件: プロセスによって取得されたリソースは、使い果たされる前に強制的に剥奪することはできません。

4. 循環待機条件: いくつかのプロセスは、リソースに対する先頭から末尾までの循環待機関係を形成します。

解決策: デッドロックの状態を打破する

楽観的ロック、リソース相互排他条件の破棄、CAS

リソースは一度割り当てられるため、リクエストとホールドの条件が剥奪され、tryLock が実行されます。

剥奪可能なリソース: つまり、プロセスの新しいリソースが満たされない場合、占有されていたリソースが解放され、それによって譲渡不可能な状態が破壊され、データベースのデッドロックがタイムアウトになります。

リソースの規則的な割り当て方法: システムは各種類のリソースにシリアル番号を割り当て、増加する番号に従って各プロセスがリソースを要求することで、ループ待ち状態と転送シナリオを破壊します

Javaでデッドロックを回避する方法

デッドロックが発生する条件は上記の4つですが、デッドロックを回避したい場合は、いずれかの条件を満たさないようにするだけで済みます。最初の 3 つの条件はロックとして満たさなければならない条件であるため、デッドロックを回避するには 4 番目の条件を破ってロック待ちのループ関係を回避する必要があります。

開発プロセス中:
1. ロックの順序に注意し、各スレッドが同じ順序でロックされていることを確認します。
2. ロックの時間制限に注意してください。ロックのタイムアウト時間を設定できます。
3. 次の点に注意してください。デッドロック チェック。これは、デッドロックをできるだけ早く発見して解決するための予防メカニズムです。


3. 楽観的ロック、CAS 思考

Java のオプティミスティック ロック メカニズム:

楽観的ロックは、悲観的ロックの反対を具体化します。これは前向きな考えであり、データは変更されないと常に信じているため、データはロックされません。ただし、楽観的ロックでは、更新時にデータが更新されたかどうかが判断されます。一般に、オプティミスティック ロックには 2 つの実装ソリューション (バージョン番号メカニズムと CAS) があります。オプティミスティック ロックは、読み取りが多く書き込みが少ないシナリオに適しており、システムの同時実行性を向上させることができますJava では、java.util.concurrent.atomicの下のアトミック変数クラスは、楽観的ロックの実装方法である CAS を使用して実装されます。

オプティミスティック ロックは、ほとんどの場合、データ バージョン (Version) 記録メカニズムに基づいて実装されます。つまり、データにバージョン識別子を追加することです。データベース テーブルに基づくバージョン ソリューションでは、これは通常、データベース テーブルに「バージョン」フィールドを追加することによって実現されます。データを読み出す際にはこのバージョン番号も読み出され、その後更新されるとこのバージョン番号が1つインクリメントされる。この際、入稿データのバージョン情報とデータベーステーブルの該当レコードの現在のバージョン情報を比較し、入稿データのバージョン番号がデータベーステーブルの現在のバージョン番号より大きい場合は、更新されていない場合は、期限切れのデータとみなされます。

CAS のアイデア:

CAS は Compare and swap の略でロック機構を使用せずにスレッド間の同期を実現できるよく知られたロックフリー アルゴリズムです。CAS スレッドはブロックされないため、ノンブロッキング同期とも呼ばれます。CAS アルゴリズムには 3 つの操作が含まれます。

読み書きする必要があるメモリ値 V、比較する値 A、書き込む値 B

V の値が A の値と等しく、A の値が V の値と等しい場合にのみ、B の値を使用して V の値が更新されます。そうでない場合は、操作は実行されません (比較と置換はアトミックです)。操作 - A と V、V と B の置換を比較)、通常、これはスピン操作、つまり常に再試行します。

短所:
同時実行性が高い場合、同時実行性の競合が発生しやすく、CAS が失敗し続けると再試行が繰り返され、CPU リソースが無駄に消費されます。

原子性:

機能制限: CAS は、単一変数の操作がアトミックであることを保証できます。Java では、スレッドの安全性を確保するために volatile キーワードを使用する必要があります。複数の変数が関係する場合、CAS は無力です。さらに、CAS 実装にはハードウェア レベルのサポートが必要です。 Java の一般ユーザーが直接使用できますが、アトミック パッケージの下のアトミック クラスを使用してのみ実装でき、柔軟性は限られています。


4. 悲観的なロック

データベース内のデータを変更する場合、他のユーザーによって同時に変更されることを避けるために、同時実行を防ぐためにデータを直接ロックするのが最善の方法です。

なぜ悲観的ロックと呼ばれるのでしょうか? これは、データ変更に対して悲観的な態度をとる同時実行制御方法であるためです。一般に、データが同時に変更される可能性は比較的高いため、変更前にデータをロックする必要があると考えられます。

悲観的ロックを使用する場合の解決策は、データ変更による競合の可能性が比較的高いと考えられるため、更新する前に、変更が完了するまで変更対象のレコードを明示的にロックし、その後ロックを解放します。ロック期間中は、自分だけが読み取りと書き込みが可能で、他のトランザクションは読み取りのみ可能で書き込みはできません。


5. 同期された基盤となる実装

使い方:主な3つの使い方

インスタンス メソッドの変更: 現在のオブジェクト インスタンスをロックするために使用されます。同期コードを入力する前に、現在のオブジェクト インスタンスのロックを取得する必要があります。

静的メソッドを変更します。つまり、静的メンバーはどのインスタンス オブジェクトにも属さず、クラス メンバーであるため、現在のクラスのロックはクラスのすべてのオブジェクト インスタンスに適用されます。

変更されたコード ブロック: 同期コード ベースに入る前に、ロック オブジェクトを指定し、指定されたオブジェクトをロックし、指定されたオブジェクトのロックを取得します。

概要: synchronized によってロックされるリソースは 2 種類のみです。1 つはオブジェクトで、もう 1 つはクラスです。

低レベルの実装:

オブジェクト ヘッダーに注目する必要があります。同期ロックの適用、ロック、ロックの解放はすべてオブジェクト ヘッダーに関連しているため、オブジェクト ヘッダーはロックを実装するための同期の基礎です。オブジェクト ヘッダーの主な構造は、オブジェクトのハッシュ コード、ロック情報、世代年齢、または GC フラグを格納するMark Word で構成されます。

ロックもさまざまな状態に分けられます。JDK6 より前は、ロックなしとロック (重量ロック) の 2 つの状態しかありませんでした。JDK6 以降は、同期が最適化され、2 つの新しい状態が追加されました。合計 4 つの状態があります: ロックなし状態。 、バイアス ロック、軽量ロック、重量ロック、このうちロックなしは状態です。ロックのタイプとステータスはオブジェクト ヘッダーのマーク ワードに記録されます。ロックの申請やロックのアップグレードなどのプロセス中に、JVM はオブジェクトのマーク ワード データを読み取る必要があります。

ここに画像の説明を挿入します
同期コード ブロックはmonitorenter 命令とmonitorexit 命令を使用して実装され、同期メソッドはフラグを使用して実装されます。

ロックの 4 つの状態:

ロックなし: MarkWord フラグ 01、どのスレッドも同期メソッド/コード ブロックを実行しないときの状態。

バイアス ロック: MarkWord フラグ 01 (ロックなしフラグと同じ)。競合が発生しない場合は、デフォルトでスキュー ロックが使用されます。JVM は CAS 操作 (比較と交換) を使用して、オブジェクト ヘッダーの Mark Word 部分にスレッド ID を設定し、このオブジェクトが現在のスレッドに偏っていることを示します。ロックの競合がある場合は、軽量のロックにアップグレードする必要があります。

軽量ロック: MarkWord フラグ 00。軽量ロックはスピン ロックを使用して実装されます。ロックを取得していないスレッドはスピンします。一定回数 (固定回数/適応型) スピンすると、ヘビーウェイト ロックにアップグレードされます。軽量ロックは、ロック オブジェクトを競合するスレッドが少なく、スレッドがロックを短期間保持するシナリオ向けに設計されています。

ヘビーウェイト ロック: オブジェクトの内部モニター (モニター) を通じて実装されます。前述したように、モニターの本質はオペレーティング システムの相互排他 (ミューテックス) に基づいています。オペレーティング システムは、スレッド間を切り替えるためにユーザー モードからカーネル モードに切り替える必要があります。 、非常に高価です。

マルチスレッドにおける同期ロックアップグレードの原理は何ですか?

ここに画像の説明を挿入します

ロック オブジェクトのオブジェクト ヘッダーには threadid フィールドがあります。初めてアクセスしたとき、threadid は空です。jvm はバイアスされたロックを保持することを許可し、threadid をそのスレッド ID に設定します。再度入力すると、まず、スレッド ID がスレッド ID と同じかどうかを確認します。スレッド ID は一貫しています。一貫している場合は、このオブジェクトを直接使用できます。一貫していない場合は、バイアスされたロックが軽量ロックにアップグレードされます。ロックはスピンループを一定回数実行した後、使用するオブジェクトObjectが正常に取得できなかった場合、この時点でロックが軽量ロックから重量ロックにアップグレードされます。同期ロックのアップグレード。

Javaのスレッドロック機構とは何ですか?

Java のロック メカニズムは、リソース競争の激しさに基づいて継続的にロックをアップグレードするプロセスです。

ロック原理:

  • ロック格納構造体:int型ステータス値(ロックステータスの変更に使用)、二重リンクリスト(待機中のスレッドの格納に使用)
  • Lock がロックを取得する処理:基本的には CAS を使用してステータス値の変更を取得しますが、その場で取得できない場合はスレッド待ちリストに入れられます。
  • Lock がロックを解放するプロセス: ステータス値を変更し、待機リストを調整します。
  • ロックはCAS+スピンを多用します。したがって、CAS の特性によれば、ロックの競合が少ない状況ではロックを使用することが推奨されます。

ロックと同期の違い:


1. ロックのロックとロック解除は、ネイティブ メソッド (オペレーティング システム関連のメソッドの呼び出し) と組み合わせて Java コードによって実装されますが、同期のロックとロック解除のプロセスは JVM によって管理されます。、ロックが他のスレッドによって占有されている場合、ロックが正常に取得されるまでのみブロックできます。Lock は、タイムアウト ロックや割り込み可能なロックなどのより柔軟な方法を提供し、ロックを取得できない場合の終了メカニズムを提供します。
3. ロック内には複数の Condition インスタンスが存在できます。つまり、複数の条件キューが存在しますが、Synchronize には条件キューが 1 つだけあります。Condition には柔軟なブロック方法も用意されており、通知を受ける前にスレッドを中断して待機時間制限を設定できます。条件キューを終了します。
4.synchronize はスレッド同期の排他モードのみを提供しますが、Lock は排他モードと共有モードの両方を提供できます。

同期した ロック
キーワード 親切
自動的にロックとロックを解除します ロックを解除するには、unlock メソッドを手動で呼び出す必要があります。
JVMレベルロック APIレベルのロック
不当なロック 公平または不公平なロックを選択できます
ロックはオブジェクトであり、ロック情報はオブジェクトに格納されます。 int型の状態でコードを識別
ロックのアップグレードプロセスがあります なし

6. ReenTrantLock の基礎となる実装

ReentrantLock は java.util.concurrent パッケージで提供されるミューテックス ロックのセットであるため、ReentrantLock クラスは Synchronized と比較していくつかの高度な機能を提供します。

説明書:

API レベルに基づくミューテックス ロックでは、lock() メソッドと lock() メソッドが try/finally ステートメント ブロックで完了する必要があります。

低レベルの実装:

ReenTrantLock の実装は、CAS 操作を周期的に呼び出すことによってロックを実装するスピン ロックです。カーネル状態に入るスレッドのブロック状態が回避されるため、パフォーマンスが向上します。スレッドがカーネルのブロッキング状態にならないように最善を尽くすことが、ロックの設計を分析し理解する鍵となります。

同期と同期の違い:

1.基礎となる実装: synchronized は、JVM レベルのロック、Java キーワードであり、モニター オブジェクト (monitorenter およびmonitorexit) を通じて実装されます。ReentrantLock は、jdk1.5 以降に提供される API レベル (java.util.concurrent.locks.Lock) ) ロック。

2.実装原理: synchronized の実装には、ロックのアップグレード、具体的にはロックフリー、バイアスロック、スピンロック、および OS からの重量ロックの適用が含まれます; ReentrantLock 実装は、CAS (CompareAndSwap) スピンメカニズムを利用することでスレッド操作を保証します。 volatile はロック機能を実装するためにデータの可視性を確保します。

3.手動で解放できるかどうか: 同期では、ユーザーが手動でロックを解放する必要はありません。同期されたコードが実行された後、システムは自動的にスレッドにロックを解放させますが、ReentrantLock ではユーザーが手動でロックを解放する必要があります。手動でロックを解除しないとデッドロック現象が発生する可能性があります。

4.中断の可否synchronized は、ロックされたコードで例外が発生するか、通常の実行が完了しない限り、中断できないタイプのロックです; ReentrantLock は中断可能です trylock を通じてタイムアウト方法を設定できます (長いタイムアウト、TimeUnit 単位)または lockInterruptibly() を配置します。 コード ブロックに移動し、割り込みメソッドを呼び出して割り込みます。

5.公平なロックが不公平なロック ReentrantLock に同期されているかどうか、公平なロックまたは不公平なロックを選択できます。新しい ReentrantLock メソッドを構築するときにブール値を渡すことで選択できます。空の場合、デフォルトは次のようになります。不公平なロックの場合は false、公正なロックの場合は true 公正なロックのパフォーマンスは非常に低いです。


7. 公平なロックと不公平なロックの違い

フェアロック:

公平なロックは当然FIFO (先入れ先出し) の原則に従い、最初に到着したスレッドが最初にリソースを取得し、後から到着したスレッドがキューに登録して待機します。

利点: すべてのスレッドがリソースを取得でき、キュー内で餓死することはありません。大きなタスクに適しています

短所: スループットが低下する キュー内の最初のスレッドを除いて、他のスレッドがブロックされる ブロックされたスレッドをウェイクアップするために CPU に負荷がかかる

不当なロック:

複数のスレッドがロックを取得した場合、ロックを直接取得しようとし、取得できない場合は待機キューに入り、取得できた場合は直接ロックを取得します。

利点: スレッドのウェイクアップによる CPU オーバーヘッドを削減でき、全体的なスループット効率が向上し、CPU がすべてのスレッドをウェイクアップする必要がないため、ウェイクアップするスレッドの数が減少します。

欠点: これにより、キューの途中にあるスレッドがロックを取得できなくなったり、長時間にわたってロックを取得できなくなる可能性があることにも気づいたかもしれません。

ここに画像の説明を挿入します

フェアロックの効率が低い理由:

フェア ロックはキューを維持する必要があり、後続のスレッドはそれをロックする必要があります。ロックがアイドル状態であっても、待機している他のスレッドがあるかどうかを最初に確認する必要があります。ハングアップするスレッドがある場合は、それを最後に追加します。キューを開始し、キューの先頭でスレッドをウェイクアップします。この場合、不当なロックよりもハングとウェイクアップが 1 つ多く発生します。

実際、スレッド切り替えのオーバーヘッドが、不公平なロックが公平なロックよりも効率的である理由です。不公平なロックはスレッドがハングする可能性を減らし、後続のスレッドは一時停止のオーバーヘッドから逃れる一定の可能性があるためです。


8. レベルロックの最適化を使用する

[1] ロック時間を短縮する: 同期的に実行する必要のないコードは、実行できる場合には同期ブロックに配置しないで、ロックをできるだけ早く解放できるようにします。

[2] ロックの粒度を下げる: このアイデアは、物理ロックを複数の論理ロックに分割して並列度を高め、それによってロックの競合を減らすというものです。その考え方は、空間を時間と交換することです。Java の多くのデータ構造は、次のような同時操作の効率を向上させるためにこの方法を使用しています。

同時ハッシュマップ:

jdk1.8 より前の Java の ConcurrentHashMap はセグメント配列を使用します: Segment<K,V>[] セグメント

セグメントは ReenTrantLock から継承しているため、各セグメントはリエントラント ロックです。各セグメントにはデータを保存するための HashEntry<K,V> 配列があります。書き込み操作を実行するときは、最初にデータを入れるセグメントを決定します。これをロックするだけで済みます。セグメント: put が実行されるとき、他のセグメントはロックされないため、配列内のセグメントの数に応じて、多くのスレッドが同時にデータを格納できるようになり、同時実行性が向上します。

[3] ロックの粗密化: ほとんどの場合、ロックの粒度を最小限に抑える必要があり、ロックの粗密化はロックの粒度を増やすことです。

ループがあり、ループ内の操作をロックする必要がある場合は、ループの外にロックを配置する必要があります。そうしないと、ループに出入りするたびにクリティカル セクションに出入りする必要があり、非常に非効率です。 ;

【4】読み取り/書き込みロックを使用する:

ReentrantReadWriteLock は読み取り/書き込みロックです。読み取り操作は読み取りロックを追加し、同時に読み取ることができます。書き込み操作は書き込みロックを使用し、単一のスレッドによってのみ書き込むことができます。

【5】CASを使用する:

同期する必要がある操作が非常に迅速に実行され、スレッドの競争が激しくない場合は、ロックによりスレッドのコンテキスト切り替えが発生するため、現時点では cas を使用する方が効率的です。同期操作自体は、スレッドのリソース獲得競争が激しくないため、volatile+cas 操作を使用するのが非常に効率的な選択となります。


9. システムレベルのロックの最適化

アダプティブスピンロック:

スピン ロックを使用すると、競合するロックがブロッキング一時停止状態になり目覚めるのを待つことによって引き起こされる、カーネル モードとユーザー モード間の切り替えの損失を回避できます。必要なのは待機 (スピン) だけですが、ロックが他のスレッドによって占有されている場合、長時間、CPU を解放しないでください。待機するとパフォーマンスのオーバーヘッドが増加します。スピン数のデフォルト値は 10 です。

上記のスピン ロック最適化手法をさらに最適化するために、スピン数は固定されなくなりました。スピン数は、同じロック上のスピン時間とロック所有者のステータス以前の

ロックの削除:

ロックの削除とは、仮想マシンのジャストインタイム コンパイラの実行中に、一部のコードの同期を必要とするロックが削除されますが、共有データに対する競合が存在しないことが検出されることを意味します。Netty のロックフリー設計パイプラインのチャネルハンドラーは、ロックの削除を最適化します。

ロックのアップグレード:

バイアスロック:

スレッドがすでにロックを占有している場合、次回ロックを取得しようとするとき、ほとんどの場合競合がないため、監視操作を実行せずに最速の方法でロックを取得します。そのため、バイアスされたロックを使用するとパフォーマンスが向上します。 ;

軽量ロック:

競争が激しくない場合は、CAS を使用してスレッド コンテキストの切り替えを回避でき、これによりパフォーマンスが大幅に向上します。

重量級ロック:

ヘビーウェイト ロックのロックおよびロック解除プロセスによって生じる損失は修正されています。ヘビーウェイト ロックは、激しい競争、高い同時実行性、および長い同期ブロックの実行時間がある状況に適しています。


10. ThreadLocal 原則

ThreadLocal の概要:

通常、作成した変数はどのスレッドからもアクセスおよび変更できます。各スレッドに独自の排他的なローカル変数を持たせたい場合はどうすればよいでしょうか? JDK で提供される ThreadLocal クラスは、この問題を解決するように設計されています。オペレーティング システムの TLAB に似ています

原理:

まず第一に、ThreadLocal はジェネリック クラスであり、あらゆる種類のオブジェクトを受け入れることが保証されています。複数の ThreadLocal オブジェクトが 1 つのスレッド内に存在できるため、ThreadLocal は実際には Map を内部的に維持します。これは、ThreadLocal によって実装される ThreadLocalMap と呼ばれる静的内部クラスです。

最後の変数は、現在のスレッドの ThreadLocalMap に配置され、ThreadLocal には存在しません。ThreadLocal は、変数値を渡す、ThreadLocalMap の単なるパッケージとして理解できます。

使用する get() メソッドと set() メソッドは、実際には、この ThreadLocalMap クラスに対応する get() メソッドと set() メソッドを呼び出します。たとえば次のような

使い方:

1) ユーザーセッションを保存する

private static final ThreadLocal threadSession = new ThreadLocal();

2) スレッドセーフの問題を解決する

private static ThreadLocal<SimpleDateFormat> format1 = new ThreadLocal<SimpleDateFormat>()

使用するシーン:

1. レイヤー間でオブジェクトを転送する場合、ThreadLocal を使用すると、複数の転送を回避し、レイヤー間の制約を解除できます。

2. スレッド間のデータ分離

3. トランザクション操作を実行して、スレッドのトランザクション情報を保存します。

ThreadLocal メモリ リークのシナリオ

実際、ThreadLocalMap で使用されるキーは ThreadLocal への弱参照であり、値は強参照です。弱参照の特徴は、このオブジェクトが弱参照を保持している場合、次回のガベージ コレクション中に必ずクリーンアップされることです。

したがって、外部から強く参照されていない ThreadLocal はガベージコレクション時にクリーンアップされ、ThreadLocalMap 内でこの ThreadLocal を使用しているキーもクリーンアップされます。ただし、value は強参照であり、クリーンアップされないため、null キーを持つ値が存在します。何も対策しないとGCで値がリサイクルされず、長時間スレッドが破壊されないとメモリリークが発生する可能性があります。

ここに画像の説明を挿入します
ThreadLocalMap の実装ではこの状況が考慮されており、set()、get()、remove() メソッドが呼び出されるとき、null キーを持つレコードはクリアされます。メモリ リークが発生する場合は、null キーを持つレコードが表示された後で、remove() メソッドが手動で呼び出されず、その後 get()、set()、および Remove() メソッドが呼び出されなくなった場合にのみ発生します。したがって、ThreadLocal メソッドを使用した後は、remove() メソッドを手動で呼び出すことをお勧めします


11. HashMap のスレッドセーフ

無限ループにより CPU 100% が発生する

HashMap に無限ループが発生し、CPU が 100% になる可能性があります。これが発生する主な理由は、拡張中、つまり新しい HashMap が内部で作成されるときに、拡張ロジックによりハッシュ バケット内のノードの順序が逆になることです。 、複数のスレッドが同時に展開している場合、HashMap はスレッドセーフではないため、2 つのスレッドが同時に反転するとループが形成される可能性があり、このループはリンク リストのループであり、これは次と同等です。ノード A は B を指しています。ノード、ノード B はノード A を指しています。このようにすると、次回キーに対応する値を取得する場合、リンク リストを走査するときに走査が終了することはなく、CPU は 100% になります。が発生する場合がございます。

要約すると、HashMap はスレッドセーフではありません。マルチスレッドを使用するシナリオでは、スレッドセーフでパフォーマンスの高い ConcurrentHashMap を使用することをお勧めします。


12、カウントダウンラッチ

CountDownLatch はカウンターを介して実装される同期ツールのクラスで、初期値はスレッド数です。スレッドがタスクを完了するたびに、それに応じてカウンタ値が 1 ずつ減らされます。カウンタが 0 に達すると、すべてのスレッドが実行を完了し、待機中のスレッドがタスクの実行を再開できることを意味します。

CountDownLatch はグローバル カウンタを初期化します。スレッドを待機させたい場合は、スレッドが countdownLatch.await() を呼び出し、他のスレッドが countdownLatch.countDown() を呼び出してカウンタを 0 になるまで減らします。実行を再開します。1 つのスレッドがスレッドのグループの実行が完了するまで待機し、1 つのスレッドがスレッドのグループを解放し、複数のスレッドが複数のスレッドを解放するというシナリオを実現できます。

方法の詳細な説明:

  • CountDownLatch(int count): count はカウンターの初期値です (通常、count は実行に必要なスレッドの数に設定されます)。
  • countDown(): カウンタが呼び出されるたびに、カウントが 0 になるまでカウンタ値は -1 になります。これは、すべてのスレッドが実行されたことを意味します。
  • getCount(): 現在のカウンター値を取得します。
  • await(): カウンタが 0 になるまで待機します。つまり、すべての非同期スレッドが実行を完了するまで待機します。
  • boolean await(long timeout, TimeUnit Unit): このメソッドと await() の違い: ①このメソッドは指定された時間だけ待機し、タイムアウト後に自動的に起動します。タイムアウトが 0 以下の場合は、 ②ブール型戻り値: if counter ゼロになった場合は true を返し、指定された待ち時間が経過した場合は false を返します。

リアルタイム システムでの使用シナリオ:

  • 最大の並列処理を実現する: 最大の並列処理を実現するために、複数のスレッドを同時に開始したい場合があります。たとえば、シングルトン クラスをテストしたいとします。初期カウンター 1 で CountDownLatch を作成し、他のすべてのスレッドをこのロックで待機させる場合、 countDown() メソッドを 1 回呼び出すだけで、待機中の他のすべてのスレッドが同時に実行を再開できるようになります。
  • N 個のスレッドがそれぞれのタスクを完了するまで待ってから、実行を開始します。たとえば、アプリケーション起動クラスは、ユーザー要求を処理する前に、N 個の外部システムがすべて起動して実行されていることを確認する必要があります。
  • デッドロックの検出: 非常に便利な使用シナリオは、各テスト フェーズで異なる数のスレッドを使用して N 個のスレッドを使用して共有リソースにアクセスし、デッドロックの生成を試みることです。

13、サイクリックバリア

CyclicBarrier は周期的なバリアとして理解でき、Cyclic はサイクルを意味し、このカウンターが繰り返し使用できることを意味します。

CyclicBarrier は、サイクリック バリアと呼ばれることがよくあります。これは CountDownLatch と非常に似ており、どちらも最初にスレッドを待機させてから実行できます。ただし、CountDownLatch では、スレッドのバッチは別のスレッドのバッチの実行が完了するまで待機してから実行されますが、CyclicBarrier では、待機中のスレッドが特定の数に達するだけで実行を継続できます。したがって、CyclicBarrier 内にもカウンターがあり、カウンターの初期値は、以下に示すように、オブジェクトの作成時に構築パラメーターを通じて指定されます。

public CyclicBarrier(int parties) {
    
    
    this(parties, null);
}

await() メソッドを呼び出すたびに、ブロックされたスレッドの数が 1 ずつ増加します。ブロックされたスレッドの数が設定値に達した場合にのみバリアが開き、ブロックされたすべてのスレッドが実行を継続できるようになります。さらに、CyclicBarrier については注意すべき点がいくつかあります。

  • CyclicBarrier のカウンターはリセットできますが、CountDownLatch はリセットできません。つまり、CyclicBarrier インスタンスは再利用できますが、CountDownLatch は 1 回しか使用できません。そして、これはワードサイクルバリアサイクルの意味論でもあります。
  • CyclicBarrier を使用すると、barrierAction 操作をカスタマイズできます。これは、CyclicBarrier オブジェクトの作成時に指定できるオプションの操作です。
 指定本局要拦截的线程数parties 及 本局结束时要执行的任务
public CyclicBarrier(int parties, Runnable barrierAction) {
    
    
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

CyclicBarrier オブジェクトの作成時にユーザーが BarrierAction パラメーターを設定すると、ブロックされたスレッドの数が設定値に達してバリアが開かれる前に、barrierAction の run() メソッドが呼び出され、ユーザー定義の操作が完了します。

一般的に使用される方法:

//参数parties:表示要到达屏障 (栅栏)的线程数量
//参数Runnable: 最后一个线程到达屏障之后要做的任务
//构造方法1
public CyclicBarrier(int parties) 
//构造方法2 指定本局要拦截的线程数parties 及 本局结束时要执行的任务
public CyclicBarrier(int parties, Runnable barrierAction)
//线程调用await()方法表示当前线程已经到达栅栏,然后会被阻塞
public int await() throws InterruptedException, BrokenBarrierException {
    
    
    try {
    
    
        return dowait(false, 0L);
    } catch (TimeoutException toe) {
    
    
        throw new Error(toe); // cannot happen
    }
}
//带时限的阻塞等待
public int await(long timeout, TimeUnit unit) throws InterruptedException,BrokenBarrierException,TimeoutException {
    
    
    return dowait(true, unit.toNanos(timeout));
}


14、セマフォ

セマフォは JDK が提供する同期ツールで、複数のライセンスを維持することで共有リソースへのスレッドのアクセスを制御します。残りのライセンス番号が 0 より大きい場合、スレッドは共有リソースへのアクセスを許可され、残りのライセンス番号が 0 の場合、スレッドは共有リソースへのアクセスを拒否されます。セマフォによって維持されるライセンスの数は、共有リソースへのアクセスが許可されるスレッドの最大数です。したがって、共有リソースにアクセスしたいスレッドは、セマフォからライセンスを取得する必要があります。一般的に使用される方法:

//默认获取一个许可
public void acquire() throws InterruptedException {
    
    
        sync.acquireSharedInterruptibly(1);
    }
//默认释放一个许可
public void release() {
    
    sync.releaseShared(1); }
//获取指定的许可数    
public void acquire(int permits) throws InterruptedException {
    
    
        if (permits < 0) throw new IllegalArgumentException();
        sync.acquireSharedInterruptibly(permits);
    }
//尝试获取指定的许可数    
public boolean tryAcquire(int permits) {
    
    
        if (permits < 0) throw new IllegalArgumentException();
        return sync.nonfairTryAcquireShared(permits) >= 0;
    }
//释放指定的许可数    
public void release(int permits) {
    
    
        if (permits < 0) throw new IllegalArgumentException();
        sync.releaseShared(permits);
    }

取得メソッドが呼び出されると、セマフォからライセンスを取得できるまでスレッドがブロックされ、その後、スレッドがライセンスを取得します。release メソッドが呼び出されると、セマフォにライセンスが追加されます。ライセンスの取得のためにスレッドがブロックされている場合は、ライセンスを取得して解放されます。ライセンスを取得するスレッドがない場合、セマフォは記録のみを行います。ライセンスの利用可能性、数量。張三、李斯、王武、趙立は一緒にレストランに食事に行きました。しかし、特別な時期には手洗いが非常に重要です。食事の前にも手を洗う必要があります。しかし、ホテルには洗面台が2つしかなく、このシナリオでは、セマフォを使用できます。
Javaのコードをコピーする

package onemore.study.semaphore;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
public class Customer implements Runnable {
    
    
    private Semaphore washbasin;
    private String name;
    public Customer(Semaphore washbasin, String name) {
    
    
        this.washbasin = washbasin;
        this.name = name;
    }
    @Override
    public void run() {
    
    
        try {
    
    
            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
            Random random = new Random();
            washbasin.acquire();
            System.out.println(
                sdf.format(new Date()) + " " + name + " 开始洗手...");
            Thread.sleep((long) (random.nextDouble() * 5000) + 2000);
            System.out.println(
                sdf.format(new Date()) + " " + name + " 洗手完毕!");
            washbasin.release();
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
}
//测试类
package onemore.study.semaphore;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Semaphore;
public class SemaphoreTester {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        //饭店里只用两个洗手池,所以初始化许可证的总数为2。
        Semaphore washbasin = new Semaphore(2);
        List<Thread> threads = new ArrayList<>(3);
        threads.add(new Thread(new Customer(washbasin, "张三")));
        threads.add(new Thread(new Customer(washbasin, "李四")));
        threads.add(new Thread(new Customer(washbasin, "王五")));
        threads.add(new Thread(new Customer(washbasin, "赵六")));
        for (Thread thread : threads) {
    
    
            thread.start();
            Thread.sleep(50);
        }
        for (Thread thread : threads) {
    
    
            thread.join();
        }
    }
}

操作結果:

06:51:54.416 李四 开始洗手...
06:51:54.416 张三 开始洗手...
06:51:57.251 张三 洗手完毕!
06:51:57.251 王五 开始洗手...
06:51:59.418 李四 洗手完毕!
06:51:59.418 赵六 开始洗手...
06:52:02.496 王五 洗手完毕!
06:52:06.162 赵六 洗手完毕!

内部原則:

セマフォは内部的には主に AQS (AbstractQueuedSynchronizer) を介したスレッド管理を実装します。セマフォを構築するときは、ライセンスの数を渡す必要があり、これは最終的に AQS の状態値に渡されます。スレッドがライセンスを取得するために取得メソッドを呼び出すとき、セマフォ内のライセンス数が 0 より大きい場合、ライセンス数は 1 減らされ、スレッドは実行を継続します。スレッドが実行を終了して release を呼び出すと、ライセンスを解放する方法の場合、ライセンス数はプラス 1 になります。ライセンス取得時にセマフォ内のライセンス数が 0 の場合、取得は失敗し、スレッドは AQS の待機キューに入り、ライセンスを解放する他のスレッドによって起動されるのを待ちます。詳細な原則:

コードでは、これら 4 人はスレッドが開始された順序で手を洗いますか?

スレッドが開始された順序で手を洗うわけではないため、趙劉が王武よりも先に手を洗う可能性があります。

理由: セマフォを使用するコンストラクターは次のとおりです。

public Semaphore(int permits) {
    
    
    sync = new NonfairSync(permits);
}

このコンストラクタではNonfairSync(アンフェアロック)を使用していますが、このクラスはスレッドがライセンスを取得する順序を保証するものではなく、acquireメソッドを呼び出したスレッドが待機しているスレッドよりも先にライセンスを取得する可能性があります。

彼らの順序を確実にする方法はありますか?

セマフォの別のコンストラクターを使用できます。

public Semaphore(int permits, boolean fair) {
    
    
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

コンストラクターを呼び出すとき、fair パラメーターは true で渡されます。次に例を示します。

Semaphore washbasin = new Semaphore(2, true);

これは、FairSync (フェア ロック) を使用して、各スレッドが取得メソッドを呼び出した順序でライセンスが取得されるようにします。

おすすめ

転載: blog.csdn.net/weixin_43228814/article/details/132637026