前回の記事では、AQS組み込みキューノードのデキュー操作とエンキュー操作、および共有リソースの排他的取得と解放の詳細なプロセスを紹介しました。構造的な整合性のために、この記事では引き続きAQSの観点から別の操作を紹介します。共有モードの取得およびリソースの解放の詳細については、この記事ではReentrantLockやReentrantReadWriteLockなどの特定のサブクラスの実装を分析せず、後で追加し続けます。
リソースへの排他的アクセス
わかりやすいリマインダー:この記事では、リソースを取得および解放するための共有モードの特性に焦点を当てています。多くのコード実装は、ロジックが共有タイプおよび排他タイプと類似しています。明確に比較するために、排他コアコードの一部をここに貼り付けます。理解に注意を払う共有と排他には違いがあります。詳細な分析については、前回の記事「Java並行パッケージソースコード学習シリーズ:CLH同期キューと同期リソースの取得とリリース」を参照してください。
voidacquire(int arg)
public final void acquire(int arg) {
if (!tryAcquire(arg) && // tryAcquire由子类实现,表示获取锁,如果成功,这个方法直接返回了
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 如果获取失败,执行
selfInterrupt();
}
ブール値acquireQueued(Node、int)
// 这个方法如果返回true,代码将进入selfInterrupt()
final boolean acquireQueued(final Node node, int arg) {
// 注意默认为true
boolean failed = true;
try {
// 是否中断
boolean interrupted = false;
// 自旋,即死循环
for (;;) {
// 得到node的前驱节点
final Node p = node.predecessor();
// 我们知道head是虚拟的头节点,p==head表示如果node为阻塞队列的第一个真实节点
// 就执行tryAcquire逻辑,这里tryAcquire也需要由子类实现
if (p == head && tryAcquire(arg)) {
// tryAcquire获取成功走到这,执行setHead出队操作
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 走到这有两种情况 1.node不是第一个节点 2.tryAcquire争夺锁失败了
// 这里就判断 如果当前线程争锁失败,是否需要挂起当前这个线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 死循环退出,只有tryAcquire获取锁失败的时候failed才为true
if (failed)
cancelAcquire(node);
}
}
リソースの独占リリース
ブールリリース(int arg)
public final boolean release(int arg) {
if (tryRelease(arg)) { // 子类实现tryRelease方法
// 获得当前head
Node h = head;
// head不为null并且head的等待状态不为0
if (h != null && h.waitStatus != 0)
// 唤醒下一个可以被唤醒的线程,不一定是next哦
unparkSuccessor(h);
return true;
}
return false;
}
void unparkSuccessor(ノードノード)
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
// 如果node的waitStatus<0为signal,CAS修改为0
// 将 head 节点的 ws 改成 0,清除信号。表示,他已经释放过了。不能重复释放。
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
// 唤醒后继节点,但是有可能后继节点取消了等待 即 waitStatus == 1
Node s = node.next;
// 如果后继节点为空或者它已经放弃锁了
if (s == null || s.waitStatus > 0) {
s = null;
// 从队尾往前找,找到没有没取消的所有节点排在最前面的【直到t为null或t==node才退出循环嘛】
for (Node t = tail; t != null && t != node; t = t.prev)
// 如果>0表示节点被取消了,就一直向前找呗,找到之后不会return,还会一直向前
if (t.waitStatus <= 0)
s = t;
}
// 如果后继节点存在且没有被取消,会走到这,直接唤醒后继节点即可
if (s != null)
LockSupport.unpark(s.thread);
}
リソースへの共有アクセス
voidacquireShared(int arg)
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) //子类实现
doAcquireShared(arg);
}
- tryAcquireShared(int)は、サブクラスが実装するためにAQSによって提供されるフックメソッドです。サブクラスは、共有リソースの取得を実現する方法をカスタマイズできます。取得ステータスが失敗した場合、戻り値は0未満、戻り値0は排他的取得を意味します。正の戻り値は、共有が取得を意味することを意味します。
- 取得が失敗した場合は、doAcquireShared(arg);のロジックを入力します。
void doAcquireShared(int arg)
これと排他的リソースacquireQueuedの違いに注意してください。
private void doAcquireShared(int arg) {
// 包装成共享模式的节点,入队
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
// 自旋
for (;;) {
final Node p = node.predecessor();
if (p == head) {
// 尝试获取同步状态,子类实现
int r = tryAcquireShared(arg);
if (r >= 0) {
// 设置新的首节点,并根据条件,唤醒下一个节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
いくつかの違いがあることがわかります。
- 共有リソースに障害が発生すると、共有モードのノードにパッケージ化されてチームに参加します。
- 先行ノードがheadの場合は、tryAcquireSharedメソッドを使用して、サブクラスによって実装される同期状態の取得を試みます。
- 取得が成功した場合、r> = 0の場合、setHeadAndPropagate(node、r)を呼び出します。このメソッドは、最初に新しい最初のノードを設定し、最初のノードをデキューしてから、次の共有モードノードを継続的にウェイクアップして同期を実現します。複数のスレッド株式取得。
次に、setHeadAndPropagateメソッドに焦点を当てます。
void setHeadAndPropagate(Node node、intpropagate)
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
// 节点出队,设置node为新的head
setHead(node);
/*
* Try to signal next queued node if:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus either before
* or after setHead) by a previous operation
* (note: this uses sign-check of waitStatus because
* PROPAGATE status may transition to SIGNAL.)
* and
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
*/
// 这个方法进来的时候propagate>=0
// propagate>0表示同步状态还可以被后面的节点获取
// h指向原先的head节点,之后h = head,h表示新的head节点
// h.waitStatus<0表示该节点后面还有节点需要被唤醒
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
// 获取下一个节点
Node s = node.next;
// 没有下一个节点或下一个节点为共享式获取状态
if (s == null || s.isShared())
// 唤醒后续的共享式获取同步状态的节点
doReleaseShared();
}
}
- 最初に元のヘッドノードを記録してから、ノードを新しいヘッドノードとして設定します。
- 元のヘッドノードまたは新しいヘッドノードの待機状態は伝播または信号であり、下向きにウェイクアップし続けることができます。
- 次のノードが共有ノードであると判断された場合は、共有リソース解放メソッドを呼び出して、後続のノードをウェイクアップします。
リソースの共有リリース
ブール値releaseShared(int arg)
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 子类实现
doReleaseShared();
return true;
}
return false;
}
doReleaseShared()
共有モードでは、リソースを取得するか解放するかを問わず、doReleaseSharedメソッドが呼び出されることがわかります。このメソッドは、共有モードでリソースを解放してノードをウェイクアップするコアメソッドであることがわかります。機能は、次のスレッドをウェイクアップするか、伝播状態を設定することです。
後続のスレッドがウェイクアップされた後、共有ロックを取得しようとします。成功した場合は、setHeadAndPropagateを呼び出して、ウェイクニングを伝播します。このメソッドの機能は、取得と解放が競合した場合に、キュー内の待機状態にあるノードを確実にウェイクアップできるようにすることです。
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
// 自旋
for (;;) {
Node h = head;
// 队列已经初始化且至少有一个节点
if (h != null && h != tail) {
int ws = h.waitStatus;
// 无论是独占还是共享,只有节点的ws为signal的时候,才会在释放的时候,唤醒后面的节点
if (ws == Node.SIGNAL) {
// cas将ws设置为0,设置失败,将会继续从循环开始
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 唤醒后继节点,unparkSuccessor这个方法上面已经解析过
unparkSuccessor(h);
}
// 如果ws为0,则更新状态为propagate,
// 之后setHeadAndPropagate读到ws<0的时候,会继续唤醒后面节点
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 如果head在这个过程中被更改了,会继续自旋
if (h == head) // loop if head changed
break;
}
}
このメソッドは、ヘッドノードに後続ノードがある場合に2つのことを行います。
- ヘッドノードの待機状態がSIGNALの場合、ヘッドノードの状態は0に設定され、キャンセルされなかった後続のノードがウェイクアップされます。
- ヘッドノードの待機状態が0の場合は、ヘッドノードの状態をPROPAGATEに設定して、ウェイクアップが正常に伝播できるようにします。
PROPAGATEの設定の効果:PROPAGATE状態は[setHeadAndPropagate](#void setHeadAndPropagate(Node node、intpropagate))で使用されます。ヘッドノードの状態がPROPAGATEに設定されると、後続のノードが新しいヘッドノードになります。伝播> 0の条件が確立されていない場合、条件h.waitStatus <0が確立され、後続のノードをウェイクアップするかどうか、つまりウェイクアップアクションを逆方向に伝播するかどうかが決定されます。
PROPAGATEの導入で解決できる問題は何ですか?
AbstractQueuedSynchronizerのソースコードの解釈については、このブログを読むことを強くお勧めします。
排他的と共有の違いの要約
共有取得と排他取得の最大の違いは、複数のスレッドが同時に同期状態を取得できるかどうかです。リソースを共有する場合、他の共有アクセスも同時に許可されます。リソースに排他的にアクセスする場合、他のアクセスも同時にブロックされます。
AQSは、サブクラスによって実装されるフックメソッドを提供します。排他的な代表的なメソッドは、tryAcquireおよびtryReleaseおよびisHeldExclusivelyメソッドであり、共有される代表的なメソッドは、tryAcquireSharedおよびtryReleaseSharedメソッドです。
AQSでのget操作とrelease操作の標準形式:
boolean acquire() throws InterruptedException{
while( 当前状态不允许获取操作 ){
if( 需要阻塞获取请求){
如果当前线程不在队列中,则将其插入队列
阻塞当前线程
}else{
返回失败
}
}
可能更新同步器的状态
如果线程位于队列中,则将其移除队列
返回成功
}
void release(){
更新同步器的状态
if( 新的状态允许某个被阻塞的线程获取成功 ){
解除队列中一个或多个线程的阻塞状态
}
}
出典:「並行プログラミングの技術」次の図は、排他的同期ステータスを取得するプロセスを示しています。
スレッドが同期リソースの競合に失敗すると、スレッドはノードとしてパッケージ化され、CLH同期キューの最後に参加して回転を続けます。一方はaddWaiter(Node.EXCLUSIVE)で、もう一方はaddWaiter(Node.EXCLUSIVE)です。 )。
同期キュー内のスレッドは、スピン時に先行ノードがヘッドノードであるかどうかを判別します。ヘッドノードnode.predecessor()== headの場合、すべてのスレッドは同期ステータスを取得しようとしますが、次のようになります。
- 排他的取得ステータスが成功すると、1つのノードのみがデキューされます。
- 共有状態が正常に取得されると、1つのノードがチームから除外されることに加えて、次のノードもウェイクアップされます。
スレッドがロジックを実行した後、同期状態を解放します。解放後、unparkSuccessor(h)は、後で起動できる後続ノードを起動します。
元のリンク:https://www.cnblogs.com/summerday152/p/14253917.html
この記事がお役に立てば幸いです。私の公式アカウントをフォローし、キーワード[インタビュー]に返信して、Javaのコアナレッジポイントとインタビューギフトパッケージをまとめてください。共有する技術的な乾物の記事や関連資料がもっとあります。みんなが一緒に学び、進歩できるようにしましょう!