抽象同期キュー AQS

AQS - ロックの低レベルのサポート

AbstractQueuedSynchronizer 抽象同期キューは AQS と呼ばれ、シンクロナイザーを実装するための基本コンポーネントです。同時実行パッケージのロックの最下層は AQS を使用して実装されます。

さらに、ほとんどの開発者は AQS を直接使用することはありませんが、その原理を知っておくとアーキテクチャ設計に役立ちます。

AQS のクラス図構造を見てみましょう。
ここに画像の説明を挿入します
図からわかるように、AQS は FIFO 双方向キューであり、ノードの先頭と末尾を通じてキューの先頭と末尾の要素を内部的に記録します。キュー要素のタイプは Node です。

Node のスレッド変数は、AQS キューに入るスレッドを格納するために使用されます。

Node ノード内の SHARED は、共有リソースを取得して AQS キューに入れるときにスレッドがブロックおよび一時停止されたことをマークするために使用されます。

EXCLUSIVE は、排他リソースを取得して AQS キューに入れられたときに中断されたスレッドをマークするために使用されます。

waitStatus は、現在のスレッド待機ステータスを記録します。これには、CANCELED (スレッドがキャンセルされた)、SIGNAL (スレッドを起動する必要がある)、CONDITION (スレッドが条件キューで待機している)、および PROPAGATE (他のノードが起動する必要がある) があります。共有リソースを解放するときに通知されます)。

prev は現在のノードの先行ノードを記録し、next は現在のノードの後続ノードを記録します。

単一の状態情報 state が AQS で維持され、その値は getState、setState、compareAndSetState 関数を通じて変更できます。

ReentrantLock の実装では、状態を使用して、現在のスレッドがロックを取得できる再入可能回数を示すことができます。

読み取り/書き込みロック ReentrantReadWriteLock の場合、状態の上位 16 ビットは読み取りステータス、つまり読み取りロックが取得された回数を表し、下位 16 ビットは、読み取りロックを取得したスレッドの再入回数を表します。書き込みロック。

セマフォの場合、状態は現在利用可能な信号の数を表すために使用されます。

CountDownlatch の場合、状態はカウンターの現在の値を表すために使用されます。

AQS には内部クラス ConditionObject があり、ロックと組み合わせてスレッド同期を実装するために使用されます。

ConditionObject は、状態値や AQS キューなど、AQS オブジェクト内の変数に直接アクセスできます。

ConditionObject は条件変数です。各条件変数は、条件変数の await メソッドを呼び出した後にブロックされたスレッドを格納するために使用される条件キュー (一方向リンク リスト キュー) に対応します。クラス図に示すように、この条件キューの先頭要素と末尾要素は、それぞれ firstWaiter と lastWaiter です。

AQS の場合、スレッド同期の鍵は、状態値 state を操作することです。

ステートがスレッドに属しているかどうかに応じて、ステートの操作方法は排他モードと共有モードに分かれます。

排他モードでリソースを取得および解放するために使用されるメソッドは次のとおりです。

  • void 取得(int arg)
  • void 中断的に取得(int arg)
  • ブール値のリリース(int arg)

共有モードでリソースを取得および解放する方法は次のとおりです。

  • voidacquireShared(int arg)
  • voidacquireSharedInterruptibly(int arg)
  • ブール値 releaseShared(int arg)

排他的メソッドを使用して取得されたリソースは、特定のスレッドにバインドされます。つまり、スレッドがリソースを取得すると、それを取得したスレッドとしてマークされます。他のスレッドがリソースを取得するために状態を操作しようとすると、それらのスレッドは、リソースを取得するために状態を操作しようとします。リソースが現在自分自身で保持されていないことがわかり、取得が失敗するとブロックされます。

たとえば、排他ロック ReentrantLock の実装では、スレッドが ReentrantLock のロックを取得すると、AQS はまず CAS 操作を使用して状態値を 0 から 1 に変更し、次に現在のロック所有者を現在のスレッドに設定します。スレッドが再度ロックを取得し、自分がロックの所有者であることが判明すると、ステータス値が 1 から 2 に変更され、これは再入可能回数を設定することを意味します。はロックの所有者ではありません。AQS ブロッキング キューに入れられてハングします。

共有モードに対応するリソースは、特定のスレッドに関連付けられません。複数のスレッドがリソースを要求すると、CAS を介してリソースを取得するために競合します。1 つのスレッドがリソースを取得すると、現在のリソースがまだ満たすことができる場合は、別のスレッドが再度そのリソースを取得します。必要に応じて、の場合、現在のスレッドは CAS メソッドを使用してそれを取得するだけで済みます。

たとえば、Semaphore セマフォの場合、スレッドが Acquire() メソッドを通じてセマフォを取得すると、まず現在のセマフォの数がニーズを満たしているかどうかをチェックします。満たしていない場合、現在のスレッドはブロッキング キューに入れられます。それが満たされると、信号はスピン CAS. 量を通じて取得されます。

排他モードでのリソースの取得および解放のプロセスは次のとおりです。

  1. スレッドが排他的リソースを取得するためにacquire(int arg) メソッドを呼び出すと、最初に tryAcquire メソッドを使用してリソースの取得を試行し、具体的には状態変数 state の値を設定します。成功すると、その値が直接返されます。失敗すると、現在のスレッドは Node.EXCLUSIVE のタイプにカプセル化されます。Node ノードは AQS ブロッキング キューの末尾に挿入され、LockSupport.park(this) メソッドを呼び出して自身を一時停止します。
    ここに画像の説明を挿入します
  2. スレッドが release(int arg) メソッドを呼び出すと、tryRelease オペレーションを使用してリソースを解放しようとします。ここでは、状態変数 state の値が設定され、LockSupport.unpark(thread) メソッドが呼び出されてアクティブ化されます。 AQS キュー内のブロックされたスレッド (スレッド)。
    アクティブ化されたスレッドは、tryAcquire を使用して、現在の状態変数 state の値がそのニーズを満たすかどうかを確認します。満たせる場合、スレッドはアクティブ化され、下向きに実行され続けます。そうでない場合は、引き続き AQS キューに配置され、一時停止中。

ここに画像の説明を挿入します
AQS クラスには、使用可能な tryAcquire メソッドと tryRelease メソッドが用意されていないことに注意してください。AQS がロック ブロックとシンクロナイザーの基本フレームワークであるのと同様に、tryAcquire と tryRelease は特定のサブクラスによって実装される必要があります。

tryAcquire と tryRelease を実装する場合、サブクラスは CAS アルゴリズムを使用して、特定のシナリオに従って状態値の変更を試行する必要があります。成功した場合は true を返し、そうでない場合は false を返します。

サブクラスは、acquire メソッドと release メソッドが呼び出されたときの状態値の増加または減少が何を意味するかを定義する必要もあります。

たとえば、AQS 実装から継承した排他ロック ReentrantLock は、ステータスが 0 の場合はロックがアイドル状態であることを意味し、1 の場合はロックが占有されていることを意味すると定義します。

tryAcquire を書き換えるときは、内部で CAS アルゴリズムを使用して、現在の状態が 0 であるかどうかを確認する必要があります。0 の場合は、CAS を使用して 1 に設定し、現在のロック保持者を現在のスレッドに設定して、true を返します。 CAS が失敗した場合は false を返します。

たとえば、AQS から継承した排他ロックに対して tryRelease を実装する場合、内部で CAS アルゴリズムを使用して現在の状態値を 1 から 0 に変更し、現在のロック ホルダーを null に設定してから true を返すか、CAS の場合は返す必要があります。失敗します。偽です。

共有モードでは、リソースを取得および解放するプロセスは次のとおりです。

  1. スレッドが共有リソースを取得するためにacquireShared(int arg)を呼び出すと、最初にtryAcquireSharedを使用してリソースの取得を試行し、具体的には状態変数stateの値を設定します。成功した場合は直接返されます。失敗した場合は、現在のスレッドは Node.SHARED 型の Node ノードにカプセル化され、それを AQS ブロッキング キューの末尾に挿入し、LockSupport.park(this) メソッドを使用してスレッド自体を一時停止します。
    ここに画像の説明を挿入します
  2. スレッドが releaseShared(int arg) を呼び出すと、tryReleaseShared オペレーションを使用してリソースを解放しようとします。ここでは、状態変数 state の値が設定され、LockSupport.unpark(thread) を使用してブロックされたスレッドがアクティブ化されます (スレッド)を AQS キューに追加します。
    アクティブ化されたスレッドは、tryReleaseShared を使用して、現在の状態変数 state の値がニーズを満たしているかどうかを確認します。満たしている場合、スレッドはアクティブ化され、下方向に実行を続けます。そうでない場合は、引き続き AQS キューに配置され、一時停止されます。
    ここに画像の説明を挿入します
    また、AQS クラスでは、使用可能な tryAcquireShared および tryReleaseShared メソッドが提供されていないことに注意することも重要です。AQS がロック ブロックおよびシンクロナイザーの基本フレームワークであるのと同様に、tryAcquireShared および tryReleaseShared は特定のサブクラスによって実装される必要があります。

サブクラスが tryAcquireShared および tryReleaseShared を実装する場合、CAS アルゴリズムを使用して、特定のシナリオに従って状態値の変更を試行する必要があります。成功した場合は true を返し、そうでない場合は false を返します。

たとえば、AQS 実装から継承された ReentrantReadWriteLock の読み取りロックが tryAcquireShared を上書きする場合、最初に書き込みロックが他のスレッドによって保持されているかどうかがチェックされ、保持されている場合は false が直接返されます。そうでない場合は、CAS が使用されて、状態の上位 16 ビット ( ReentrantReadWriteLock では、状態の上位 16 ビットは読み取りロックが取得された回数です)。

たとえば、AQS 実装から継承された ReentrantReadWriteLock の読み取りロックが tryReleaseShared を上書きする場合、内部で CAS アルゴリズムを使用して、現在の状態値の上位 16 ビットを 1 減分してから true を返す必要があります。失敗すると false が返されます。

上で説明したメソッドの書き換えに加えて、AQS に基づいて実装されたロックは、ロックが排他的であるか現在のスレッドによって共有されているかを判断するために isHeldExclusively メソッドを書き換える必要もあります。

さらに、排他モードの voidacquire(int arg) と voidacquireInterruptibly(int arg)、および共有モードの voidacquireShared(int arg) と voidacquireSharedInterruptibly(int arg) についても興味があるかもしれません。 Interruptibly キーワードを含む関数では、このキーワードがある場合とない場合の違いは何でしょうか?それについて話しましょう。

実際、Interruptibly キーワードのないメソッドは、割り込みに応答しないことを意味します。つまり、スレッドがリソースを取得するために Interruptibly キーワードのないメソッドを呼び出した場合、またはリソースの取得が失敗して中断され、他のスレッドがスレッドに割り込んだ場合です。の場合、スレッドは割り込みが発生したため例外はスローされません。リソースの取得を続行するか、中断されます。つまり、割り込みに応答せず、割り込みを無視します。

Interruptibly キーワードを持つメソッドは、割り込みに応答する必要があります。つまり、スレッドがリソースを取得するために Interruptibly キーワードを持つメソッドを呼び出したとき、またはリソースの取得が失敗して中断され、他のスレッドがスレッドに割り込んだとき、スレッドはスローします。 InterruptedException。例外とともに返されます。

最後に、AQS が提供するキューを維持する方法を、主にエンキュー操作に注目して見てみましょう。

  • エンキュー操作: スレッドがロックの取得に失敗すると、スレッドはノード ノードに変換され、enq (最終ノード ノード) メソッドを使用してノードが AQS ブロッキング キューに挿入されます。
    ここに画像の説明を挿入します
    コードとノード図に基づいてチームに参加するプロセスを説明します。
    ここに画像の説明を挿入します

上記コードの最初のループで、AQS キューの最後に要素を挿入する場合、AQS キューのステータスは図のようになります (デフォルト)。

つまり、キューの先頭ノードも末尾ノードもnullを指しており、コード(1)を実行するとノードtは末尾ノードを指し、このときのキューの状態は図のようになる。

このとき、t は null なので、コード (2) が実行され、CAS アルゴリズムを使用してセンチネル ノードがヘッド ノードとして設定され、CAS 設定が成功すると、テール ノードもセンチネル ノードを指します。今回のキューの状態は図の(II)のようになっています。

ここまではセンチネルノードが1つしか挿入されておらず、ノードノードを挿入する必要があるため、2回目のループ後にコード(1)が実行されますが、このときのキューの状態は図(II)のようになります。

次に、コード(3)を実行して、ノードの先行ノードを末尾ノードに設定しますが、このときのキューの状態は図の(IV)のようになります。

次に、CAS アルゴリズムを通じてノードノードを末尾ノードとして設定し、CAS が成功すると、キューのステータスは図の (V) に示すようになります。

CAS成功後、元の末尾ノードのバックドライブノードをnodeに設定すると、二重リンクリストの挿入が完了し、キューの状態は図の(VI)のようになります。

AQS - 条件変数のサポート

Notify および wait は、同期された組み込みロックによるスレッド間同期を実現するために使用されるインフラストラクチャです。条件変数の signal メソッドおよび await メソッドも、ロック (AQS を使用して実装されたロック) によるスレッド間同期を実現するために使用されるインフラストラクチャです。

両者の違いは、synchronized は同時に共有変数の Notice メソッドまたは wait メソッドとのみ同期できるのに対し、AQS のロックは複数の条件変数に対応できることです。

シェア変数の Notice メソッドと wait メソッドを呼び出す前に、まずシェア変数の組み込みロックを取得する必要があります。同様に、条件変数の signal メソッドと await メソッドを呼び出す前に、まず条件に対応するロックを取得する必要があります変数。

ここに画像の説明を挿入しますコード (1) は、AQS に基づいて実装された排他ロック ReentrantLock オブジェクトを作成します。

コード(2)では、作成したLockオブジェクトのnewCondition()メソッドを利用して、Lockロックに対応する条件変数ConditionObject変数を作成しています。Lock オブジェクトは複数の条件変数を作成できることに注意してください。

コード (3) で最初に排他ロックを取得し、コード (4) で条件変数の await() メソッドを呼び出して、現在のスレッドをブロックして一時停止します。

他のスレッドが条件変数の signal メソッドを呼び出すと、ブロックされたスレッドは await から戻ります。

なお、ロックを取得する前に条件変数のawaitメソッドを呼び出した場合も、Objectのwaitメソッドを呼び出した場合と同様に、java.lang.IllegalMonitorStateExceptionがスローされます。

コード (5) は、取得したロックを解放します。

実際、ここでの Lock オブジェクトは、同期変数と共有変数に相当します。lock.lock() メソッドの呼び出しは、同期ブロックに入り (共有変数の組み込みロックを取得)、lock.unLock() メソッドを呼び出すことと同じです。 ) メソッドは、synchronized.piece を終了するのと同じです。

条件変数の await() メソッドの呼び出しは、共有変数の wait() メソッドの呼び出しと同等であり、条件変数の signal メソッドの呼び出しは、共有変数の Notice() メソッドの呼び出しと同等です。

条件変数の signalAll() メソッドの呼び出しは、共有変数の NoticeAll() メソッドの呼び出しと同じです。

上記の説明で、条件変数とは何か、そしてそれが何に使用されるのかはすでに皆さんは理解していると思います。

上記のコードでは、lock.newCondition() の関数は実際には、AQS 内で宣言された ConditionObject オブジェクトを新規作成することになります。ConditionObject は AQS の内部クラスであり、AQS 内の変数 (状態変数 state など) やメソッドにアクセスできます。

条件キューは各条件変数内に維持され、条件変数の await() メソッドを呼び出すときにブロックされたスレッドを格納します。

この条件付きキューは AQS キューと同じものではないことに注意してください。

次のコードでは、スレッドが条件変数の await() メソッドを呼び出すと (ロックを取得するには、最初にロックの lock() メソッドを呼び出す必要があります)、Node.CONDITION 型のノードノードが内部的に構築されます。その後、ノードが条件に挿入されます。キューの最後で、現在のスレッドは取得したロックを解放し (つまり、ロックに対応する状態変数の値を操作します)、ブロックされて中断されます。

このとき、別のスレッドがlock.lock()を呼び出してロックを取得しようとすると、一方のスレッドがロックを取得し、ロックを取得したスレッドが条件変数のawait()メソッドを呼び出すと、そのスレッドもロックを取得します。変数のブロッキングキューは、取得したロックを解放し、await() メソッドでブロックします。

ここに画像の説明を挿入します
次のコードでは、別のスレッドが条件変数のシグナル メソッドを呼び出すと (ロックを取得するには、最初にロックの lock() メソッドを呼び出す必要があります)、条件キューの先頭にあるスレッド ノードが内部的に削除されます。条件キューを AQS のブロッキング キューに入れて、このスレッドをアクティブにします。
ここに画像の説明を挿入します
AQS は、ConditionObject の実装のみを提供し、新しい ConditionObject オブジェクトの作成に使用される newCondition 関数は提供しないことに注意してください。

newCondition 関数は、AQS のサブクラスによって提供される必要があります。

条件変数の await() メソッドを呼び出してブロックされたスレッドを条件キューに入れる方法を見てみましょう。

ここに画像の説明を挿入します
コード (1) では、まず現在のスレッドに基づいて Node.CONDITION 型のノードを作成し、次にコード (2) (3) (4) を通じて一方向条件キューの最後に要素を挿入します。

注: 複数のスレッドが同時に lock.lock() メソッドを呼び出してロックを取得すると、1 つのスレッドだけがロックを取得します。他のスレッドは Node ノードに変換され、ロック ロックに対応する AQS ブロッキング キューに挿入されます。 、スピン CAS を試行し、ロックを取得します。

ロックを取得したスレッドが、対応する条件変数の await() メソッドを呼び出すと、スレッドは取得したロックを解放し、Node ノードに変換され、条件変数に対応する条件キューに挿入されます。

このとき、lock.lock()メソッドの呼び出しによりAQSキュー内でブロックされていたスレッドは、解放されたロックを取得しますが、そのスレッドが条件変数のawait()メソッドも呼び出した場合には、そのスレッドもAQSキューに配置されます。条件変数。条件キュー内。

別のスレッドが条件変数の signal() または signalAll() メソッドを呼び出すと、条件キュー内の 1 つまたはすべてのノード ノードを AQS ブロッキング キューに移動し、ロックを取得する機会を待ちます。

最後に、図を使用して次のように要約します: ロックは AQS ブロッキング キューと複数の条件変数に対応します。各条件変数には独自の条件キューがあります。

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

おすすめ

転載: blog.csdn.net/zhuyufan1986/article/details/135460946