Deadly Java並行プログラミング(7):ReentrantReadWriteLockソースコード分析

この記事では、Java 8に基づくReentrantReadWriteLockのソースコード分析を見てみましょう。

推奨事項を読む:Java並行性パッケージのロックはAQSに基づいて実装されているため、この記事の読み取り/書き込みロックも例外ではありません。まだ理解していないと読みにくくなります。AbstractQueuedSynchronizerソース解像度に関する記事を読むことをお勧めします
AQSの詳細な説明。今回は、Javaコンカレントパッケージでのロックの原理を完全に理解するため、インタビューごとに覚えておく必要はありません。

読み書きロックとは何ですか?

ロックに関して言えば、排他的ロックである同期キーワードReentrantLockなどの実装について考えることができます。つまり、同時にアクセスできるスレッドは1つだけで、読み取り/書き込みロックによって複数のリーダースレッドが同時にアクセスできますが、ライタースレッドがアクセスすると、リーダースレッドとライタースレッドの両方がブロックされます。読み取り/書き込みロックは、1つの読み取りロックと1つの書き込みロックのペアのロックを維持します。読み取り/書き込みロックの分離により、排他ロックと比較して同時実行性が大幅に向上します。

読み取り/書き込みロックの存在の重要性は、一般に、読み取りシーンが書き込みシーンよりもはるかに大きいということです。したがって、読み取りが書き込みよりも大きいシナリオでは、排他ロックよりも高い並行性とスループットが得られます。Java並行性パッケージで提供される読み取り/書き込みロックの実装はReentrantReadWriteLockです。

ReentrantReadWriteLockの主な機能は次のとおりですまず最初に一般的な理解を得てから、ソースコードを詳細に分析します。

特性 説明
公正な選択 フェアモードとアンフェア(デフォルト)の2つのロック取得方法をサポートし、アンフェアモードでのスループットが大きい
再入力 再入をサポートします。つまり、読み取りスレッドは読み取りロックを取得した後も読み取りロックを取得し続けることができ、書き込みスレッドは書き込みロックを取得した後も書き込みロックを取得し続けることができ、同時に読み取りロックを取得することもできます。
ダウングレードをロック 最初に書き込みロックを取得し、読み取りロックを取得し、書き込みロックを解放するシーケンスに従って、書き込みロックを読み取りと書き込みにダウングレードするプロセスを実現します。

読み書きロックインターフェースと使用例

ReadWriteLock読み取りロックと書き込みロックを取得するための2つのメソッド、つまり、readLock()メソッドとwriteLock()メソッドのみが定義さ
れています。また、インターフェースメソッドに加えて、その実装-ReentrantReadWriteLock によって、
内部の動作ステータスを簡単に監視できます。メソッドは次のとおりです。

メソッド名 説明
getReadLockCount() スレッドは読み取りロックを複数回取得できるため、保持されている読み取りロックの数、ロックを保持していないスレッドの数、取得された読み取りロックの総数
getReadHoldCount() 現在のスレッドがThradLocalに保存されている読み取りロックを保持する回数
isWriteLocked() 書き込みロックが取得されているかどうかを確認する
getWriteHoldCount() 現在のスレッドが保持している書き込みロックの数を返します

使用例

次の例は、読み書きロックの使用を非常に鮮明に示しています。

ここに画像の説明を挿入

読み取り操作のget(String key)メソッドでは、読み取りロックを取得する必要があります。これにより、メソッドへの同時アクセスがブロックされなくなります。書き込み操作のput(文字列キー、オブジェクト値)メソッドとclear()メソッドでは、HashMapを更新するときに事前に書き込みロックを取得する必要があります。

キャッシュは、読み取り/書き込みロックを使用して読み取り操作の同時実行性を向上させ、各書き込み操作のすべての読み取りおよび書き込み操作の可視性を確保し、プログラミング方法を簡略化します。

ReentrantReadWriteLockの概要

ここに画像の説明を挿入

上の図の情報をよく見てください。読み取り/書き込みロックは2つの内部ネストクラスインスタンスに対応し、カスタム同期シンクロナイザーはAQSを継承します。ReadLockとWriteLockは同期インスタンスを共有します。

ReadLockとWriteLockの特定のコードを見て、それをより明確に理解しましょう。

ここに画像の説明を挿入

ReadLockおよびWriteLockのメソッドがSyncクラスを介して実装されていることは明らかです。SyncはAQSのサブクラスであり、フェアモードとアンフェアモードを派生させます。

彼らが呼び出すSyncメソッドから、ReadLockは共有モードを使用しWriteLockは排他モードを使用することがわかります

ここで、同じSyncインスタンスには1つの状態同期状態しかありませんが、共有モードと排他モードを同時に使用するにはどうすればよいでしょうか。

上記の質問を理解できない場合は、AQSに慣れていない可能性があります。ここでは、AQSの共有モードと排他モードのプロセスを簡単に示します。水平に比較できます。

ここに画像の説明を挿入

AQSによるロックの実現の本質は、内部プロパティの状態が維持されることにあります。

  1. 同期ステータスの排他的取得の場合、0はロックを取得できることを意味し、1はロックが他人に奪われて取得できないことを意味し、現在の再エントリは可能です。
  2. 同期状態への共有アクセス。各スレッドは状態に対して加算および減算操作を実行できるため、排他型との違いは、スレッドセーフな操作の同期状態を保証することです。これは通常、ループとCASによって保証されます。

つまり、排他モードと共有モードでは、状態に対する操作がまったく異なりますが、読み取り/書き込みロックReentrantReadWriteLockで状態をどのように使用しますか?心配しないで、下を見続けてください。このデザインはとても賢いです。

読み書きロックのソースコード分析

このソースコード分析には、読み取り/書き込み状態の設計書き込みロックの取得と解放読み取りロックの取得と解放およびロックダウングレードが含まれます。

1.状態設計の読み取りと書き込み

上記のように、読み取り/書き込みロックのカスタムシンクロナイザーは、複数の読み取りスレッドと1つの書き込みスレッドの状態を同期状態(整数変数)に維持する必要があります。読み書きロックは、32ビット状態を上位16ビットと下位16ビットに分割し、それぞれ読み取りと書き込みを示します。

では、読み書きロックはどのようにして現在の読み書きステータスをすばやく判断するのでしょうか。答えはビット操作です。
現在の同期ステータス値がSであると仮定すると、書き込みステータスはS&0x0000FFFFに等しく(上位16ビットはすべて消去されます)、読み取りステータスはS >>> 16に等しくなります(符号なし0は右に16ビットシフトされます)。1による書き込みステータスが増加すると、それはS + 1に等しいとき、および1によるリードステータスが増加し、それは、S +(1 << 16)に等しい場合、S + 0x00010000です。

状態の分割に従って、推論を引き出すことができます。Sが0に等しくない場合、書き込み状態(S&0x0000FFFF)が0に等しい場合、読み取り状態(S >>> 16)は0より大きい、つまり読み取りロックが取得されています。

この結論は非常に重要であり、次のコードに反映されます。

上記の基礎があれば、以下の詳細は省略し、直接トピックに移動して、ソースコードでの実装方法を確認します。コードはそれほど多くありません。上記を理解している場合は、コードを1行ずつ確認してください。

2.書き込みロックの獲得と解放

  • 書き込みロックは排他ロックです。
  • 読み取りロックが占有されている場合、書き込みロックの取得はブロッキングキューに入り、待機する必要があります。

書き込みロックの取得

ReentrantReadWriteLock読み取り/書き込みロックのカスタムシンクロナイザーSyncによって実装される書き込みロック取得メソッドを最初に見てみましょう。

ここに画像の説明を挿入

writerShouldBlock()の判断を見てみましょう。コードのコメントは一目でわかります

ここに画像の説明を挿入

上記のコードを理解しているはずですが、読み取りロックが取得されている場合に書き込みロックを取得できない理由について説明します。

これは主に、設計の本来の意図および使用シーンと組み合わされます。読み取り/書き込みロックは、書き込みロックの操作が読み取りロックに表示されることを保証する必要があります。読み取りロックが取得され、書き込みロックが他のスレッドによってまだ取得されている場合、読み取りロックを取得したスレッドはそれを認識できません。書き込みロックスレッドを取得する操作。

ロック解除の書き込み

次に、書き込みロックの解放を確認します。

ここに画像の説明を挿入

3.読み取りロックの獲得と解放

  • 読み取りロックは共有ロックです。
  • 読み取りロックは複数のスレッドで同時に取得でき、書き込みステータスが0(書き込みロックが取得されていない)の場合、読み取りロックは常に正常にアクセスされます。

読み取りロックのソースコードの取得は比較的複雑です。Java5からJava 6への移行はさらに複雑になりました。主な理由は、現在のスレッドが読み取りロックを取得した回数を返すgetReadHoldCount()メソッドなど、いくつかの新しい関数が追加されたことです。読み取りステータスは、すべてのスレッドが読み取りロックを取得する回数の合計であり、各スレッドが読み取りロックを取得する回数はThreadLocalにのみ保存でき、スレッド自体が維持できるため、読み取りロックの取得の実装が複雑になります。

結局のところ、書き込みロックの取得は比較的単純であるため、読者の信頼を大幅に高めることができます。次に、この読み取りロックの実現を見てみましょう。

読み取りロックの取得

ReadLockのロックプロセスを以下に示します。

ここに画像の説明を挿入

上記のコードは、主にtryAcquireShared(arg)メソッドを理解するためのものです。

AQSでは、tryAcquireShared(arg)メソッドの戻り値が0より小さい場合は共有ロック(読み取りロック)が取得されていないことを意味し、0より大きい場合は取得されていることを意味します。

上記のコードで、if分岐に入る(つまり、読み取りロックを取得する)には、満たす必要があります。readerShouldBlock()はfalseを返し、CASは成功する必要があります(MAX_COUNTオーバーフローについては気にしないでください)。

上記のプロセスに基づいて、fullTryAcquireShared(current)メソッドを入力する方法を考えますか?

  • readerShouldBlock()は、2つの場合にtrueを返します。

FairSyncが言うのはhasQueuedPredecessors()です。つまり、ロックを待機しているブロッキングキューに他の要素があります。つまり、フェアモードでは、誰かがキューイングを行っており、新しいメンバーが直接ロックを取得することはできません。

NonFairSyncが言うことは明らかにFirstQueuedIsExclusive()です。つまり、ブロッキングキューのヘッドの最初の後続ノードが書き込みロックを取得するかどうかを決定します。そうである場合は、書き込みロックが最初に来るようにして、書き込みロックの枯渇を回避します。作成者は書き込みロックの優先度を高く定義しているため、書き込みロックを取得するスレッドがロックを取得しようとしている場合、読み取りロックを取得するスレッドはそれを取得しないでください。head.nextが書き込みロックを取得しない場合は、自由に取得できます。これは不公平なモードであるため、誰もがCASより高速です。

  • compareAndSetState(c、c + SHARED_UNIT)ここでCASは失敗し、競合が発生しています。それは別の読み取りロック取得と競合する可能性があり、もちろん別の書き込みロック取得操作と競合する可能性があります。

その後、それはfullTryAcquireSharedになり、再試行します。

ここに画像の説明を挿入

上記のソースコード分析は非常に詳細である必要があります。上記の場所のコメントが理解できない場合は、ここで要約します。最初の読み取りロックの取得をキャッシュするために使用されるfirstReader、cachedHoldCounterを削除しますスレッドと、読み取りロックを取得した最後のスレッドは、パフォーマンスを向上させるために本質的に使用されます。原則はおそらくこれです:通常、読み取りロックの取得はすぐにリリースを伴います。当然、取得->読み取りロックのリリースではこの間、他のスレッドが読み取りロックを取得しない場合、このキャッシュはThreadLocalでマップを照会する必要がないため、パフォーマンスの向上に役立ちます。

コアプロセスを要約します。

ここに画像の説明を挿入

ロック解除の読み取り

読み取りロックの解放プロセスを見てみましょう。

ここに画像の説明を挿入

ロック解放の読み取りプロセスは比較的単純です。主なことは、現在のスレッドによって保持されている読み取りロックの数から1を引くことです。0に減少した場合、対応するHoldCounterをThreadLocalから削除する必要があります。

次に、forループで、状態の上位16ビットが1減算されます。読み取りロックと書き込みロックの両方が解放されていることがわかった場合、書き込みロックを取得した後続のスレッドが起こされます。

ダウングレードをロック

ロックの低下とは、読み取りロックに対する書き込みロックの低下を指します。現在のスレッドに書き込みロックがあり、それを解放し、最後に読み取りロックを取得する場合、このセグメント化された完了のプロセスをロックの低下と呼ぶことはできません。ロックの低下とは、(現在所有されている)書き込みロックを保持し、読み取りロックを取得してから、(以前に所有されていた)書き込みロックを解放するプロセスを指します。

ロックの低下の例を見てみましょう。

ここに画像の説明を挿入

上記の例では、データが変更されると、更新変数(booleanおよびvolatile modified)がfalseに設定されます。現時点では、processData()メソッドにアクセスするすべてのスレッドが変更を認識できますが、書き込みロックを取得できるのは1つのスレッドだけです。他のスレッドは、読み取りロックと書き込みロックのlock()メソッドでブロックされます。現在のスレッドは書き込みロックを取得してデータの準備を完了した後、読み取りロックを取得し、書き込みロックを解放してロックのダウングレードを完了します。

ロック低下時に読み取りロックを取得する必要がありますか?
現在のスレッドが読み取りロックを取得せずに直接書き込みロックを解放した場合、別のスレッドが書き込みロックを取得してデータを変更すると、現在のスレッドは書き込みの取得を認識できないため、データの可視性を確保するために答えが必要です。ロックスレッドによって行われた変更。

総括する

  1. 読み取り/書き込みロックは、読み取りロックと書き込みロックを定義します。これらは、書き込みロックと読み取りロックを同時に保持できますが、その逆はできません。
  2. 読み取りロックが保持されている場合、書き込みロックの取得は必然的に失敗し、ブロッキングキューに入ります。理解を深めるために、書き込みロック取得のソースコードtryAcquire(int acquire)を表示できます。
  3. 読み取りロックを取得するときに、書き込みロックが取得されていても、書き込みロックを取得したスレッドが現在のスレッドである場合は、読み取りロックを取得できます。
  4. 読み取り/書き込みロックのソースコード分析、読み取りロックの取得は理解が困難です。これは主に、jdk1.6が現在のスレッドロックを取得する回数などの関数を導入し、各スレッドの読み取りステータスは、スレッド自体によって維持されるThradLocalにのみ保存できるためです。同時に、ほとんどの場合にロック競合を取得する可能性が低いことを考慮して、firstReader、cachedHoldCounterなどを導入して、最初と最後の読み取りロックスレッドと再入可能時間をキャッシュします。ソースコードを表示するとき、私のコメントは非常に詳細である必要があり、この考え方で表示することは理解できるはずです。

(全文の終わり)戦い!

個人公開口座

ここに画像の説明を挿入

  • 彼らが上手に書いていると感じている友人は気に入ることができて、フォローすることができます
  • 記事が正しくない場合は、指摘してください。読んでいただきありがとうございます。
  • 私は公式アカウントに注意を払うことをお勧めします。私は定期的にオリジナルの乾物記事をプッシュし、あなたを高品質の学習コミュニティに引き込みます。
  • githubアドレス:github.com/coderluojust/qige_blogs

おすすめ

転載: blog.csdn.net/taurus_7c/article/details/105891774