1.フロンティア
AQSのフルネームはAbstractQueuedSynchronizer、つまり抽象キューシンクロナイザーです。その最下層はCASテクノロジーを使用して操作のアトミック性を確保すると同時に、FIFOキューを使用してスレッド間のロック競合を実現します。基本機能として->同期はAQSに抽象化されます。これは、JDK同時実行パッケージのReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLockなどの同期ツールによる同期の基盤となる実装メカニズムでもあります。したがって、AQSは非常に重要な同期基本抽象クラスです。 JDKコンカレントパッケージのコア基本コンポーネントの一部です。そのソースコードについて学びましょう。
2.AQS構造
AQSの中核は、CAS操作の同期ステータスとFIFO同期キューを介してスレッド間ロックの競合を実現することです。主に排他ロックと共有ロックの取得と解放を実現します。これらは、同期操作のためにAQSによって実装されるいくつかの一般的な方法です。上位層の同期。ツールは多くの詳細をシールドし、ReentrantLockとCountDownLatchを使用するためのしきい値を大幅に削減します。java.util.concurrent.locks.AbstractQueuedSynchronizerの構造は次のとおりです。
いくつかの重要な分野とコアメソッドは次のとおりです。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
/**
* Head of the wait queue, lazily initialized. Except for
* initialization, it is modified only via method setHead. Note:
* If head exists, its waitStatus is guaranteed not to be
* CANCELLED.
*/
// 队列头(当前执行的节点),延迟初始化,除了初始化只能通过 setHead 方法修改
private transient volatile Node head;
/**
* Tail of the wait queue, lazily initialized. Modified only via
* method enq to add new wait node.
*/
// 队列尾结点,延迟初始化,只能通过 enq 方法添加新节点
private transient volatile Node tail;
/**
* The synchronization state.
*/
// 同步状态
private volatile int state;
/**
* Atomically sets synchronization state to the given updated
* value if the current state value equals the expected value.
* This operation has memory semantics of a {@code volatile} read
* and write.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that the actual
* value was not equal to the expected value.
*/
// 大名鼎鼎的 CAS 方法
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
}
head:待機キューのヘッドノード。現在実行中のノードを表します。
tail:待機キューのテールノード
state:同期状態。state > 0はロック状態です。ロックがロックされるたびに、元の状態に1が追加されます。これは、現在ロックを保持しているスレッドが状態ロックを追加したことを意味します。逆に、減少します。ロックが解除されるたびに1ずつ状態= 0はロックフリー状態です
compareAndSetState:CASメソッドは、状態の同期状態を変更して、状態のアトミック操作を保証します
ヘッド、テール、および状態フィールドはすべて、複数のスレッド間の可視性を確保するために揮発性で変更されます
ヘッドノードは、現在ロックを保持しているスレッドのノードを表します。残りのスレッドがロックの競合に失敗した後、それらはキューの最後に参加します。テールは常にキューの最後のノードを指します。
3、AQSソースコード
AQSソースコードと言えば、キューのコアコンポーネントであるコア内部ノードノードと言わなければなりません。ソースコードを見てみましょう。
3.1ノードコア内部クラス
ノードの全体的な構造は次のとおりです。
次の図に示すように、ノードコードのコメントでは、作成者がキューモデルを簡潔なグラフィックで説明しています。
上の図から、Nodeは二重にリンクされたリスト構造であり、フィールドは次のように記述されていることがわかります。
共有:共有ロックノード
EXCLUSIVE:排他ロックノード
waitStatus:ノードステータス。以下に示すように、合計4つの値があります:
1)、CANCELED(1):キャンセル状態。現在のスレッドのフロントノードのステータスがCANCELLEDの場合、フロントノードがタイムアウトを待機しているか中断されていることを意味し、フロントノードをキューから削除する必要があります。現時点では
2)、SIGNAL(-1):トリガー状態を待機しています。現在のスレッドのフロントノードの状態がSIGNALの場合、フロントノードがこれでロックを取得するため、現在のスレッドをブロックする必要があることを意味します。時間
3)、CONDITION(-2):条件状態を待機しています。これは、現在のノードが条件を待機している、つまり条件キューにあることを意味します。
4)PROPAGATE(-3):状態は逆方向に伝播され、共有ロックでのみ使用されます。つまり、ロックが解放されたときに後続のノードに伝播する必要があります。
prev:プレノード
次へ:投稿ノード
スレッド:現在のスレッドオブジェクト
predecessor:現在のノードのpredecessorを取得します
排他ロックと共有ロックのソースコードは似ていますが、排他ロックを例に取って、ロックの取得と解放のソースコードを分析してみましょう。
排他的ロック:スレッドがロックを取得すると、他のスレッドはロックの取得に失敗し、他のスレッドは待機キューに入り、ロックが取得されるのを待ちます
3.2ロックの取得
ロックを取得するための全体的なフローチャートは次のとおりです。
ロックを取得するためのエントリポイントはacquireメソッドです。ソースコードは次のとおりです。
public final void acquire(int arg) {
// 1、tryAcquire 方法需要自己实现逻辑,功能是获取锁
// 2、addWaiter 方法将当前线程封装成Node节点,Node.EXCLUSIVE 表示独占锁
// 3、acquireQueued 方法中试图再次获取锁,因为此时前置节点可能已经释放锁了
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 4、获取锁失败,阻塞当前线程
selfInterrupt();
}
取得メソッドのコードは非常に小さいですが、次の重要なロジックを実装しています。
1)tryAcquire(arg)メソッドは、ロックの取得を試みます。このメソッドは、クラス自体のロジックを実装する必要があります。ロックの取得が成功すると、直接戻ります。それ以外の場合は、2つの手順を実行します。
2)最初にaddWaiter(Node.EXCLUSIVE)メソッドを呼び出して、現在のスレッドを排他ロックタイプのノードノードにカプセル化し、キューの最後に(エンドノードとして)追加してから、acquireQueuedメソッドを実行してロックを取得します再び
3)、acquireQueuedロックを取得するロジックは、現在のノードのフロントノードがヘッドノードであるかどうかを判断し、ヘッドノードの場合はロックを取得します。ロックが正常に取得されると、現在のノードは次のようになります。ヘッドノードとして設定し、falseを返します
4)現在のロック取得が失敗した後、selfInterruptメソッドが最終的に呼び出されて自身をブロックします
次に、次のように、addWaiter(Node.EXCLUSIVE)メソッドのソースコードを確認します。
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
// 构建 Node 节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
// 尾结点存在
node.prev = pred;
// 使用 CAS 操作将当前节点设置为尾结点,由于使用 CAS 操作则多线程情况下只会有一个线程执行成功,其余线程则会直接去调用 enq 方法添加节点
if (compareAndSetTail(pred, node)) {
// 设置成功后,旧尾结点的下一个节点指针指向当前节点
pred.next = node;
// 返回当前节点
return node;
}
}
// 尾结点不存在
enq(node);
return node;
}
addWaiterメソッドは、主に次のロジックを実装します。
1)、現在のスレッドは排他ロックタイプのノードにカプセル化されます
2)現在のノードをキューの最後に追加し、それを終了ノードとして設定します
次に、下を向いてノードenqメソッドを追加します。ソースコードは次のとおりです。
/**
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
private Node enq(final Node node) {
// 自旋,保证当前节点添加到队尾并设置为尾结点
for (;;) {
Node t = tail;
if (t == null) {
// Must initialize
// 尾结点不存在,即表示队列为空,此时初始化队列
// 初始化队列,先创建一个新的Node,设置为头结点,设置成功之后,将尾结点也指向头结点
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 队列不为空,直接将当前节点添加到队尾并设置为尾结点
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
// 当前节点添加到队尾成功之后,结束自旋操作,返回旧尾结点
return t;
}
}
}
}
enqメソッドは、主に次のロジックを実装します。
1)for(;;)spinは、率直に言って、無限ループです。これは、現在のノードがキューの最後に追加されるようにするための、AQSの非常に重要なメカニズムです。
2)キューのテールノードが空の場合は、キューを初期化し、ヘッドノードを空のノードに設定します。ヘッドノードとは、現在実行中のノードを意味します。
3)チームのテールノードが空でない場合、CAS操作が実行され、現在のノードがチームのテールに追加されます。失敗した場合は、成功するまでスピンを続けます。
この時点で、addWaiterメソッドのロジック全体が分析されます。要約は次のとおりです。
1)チームの終わりが空でない場合は、CAS操作を実行して、現在のノードをチームの終わりに追加します。チームの終わりへの参加が失敗した場合は、引き続きenqメソッドを呼び出してチームに参加します。
2)チームの終わりが空でない場合、またはチームの終わりに参加できなかったノードがenqメソッドを呼び出してチームに参加する場合
3)スピン戦略は、現在のノードがキューの最後に参加することを保証するためにenqメソッドで使用されます
4)キューのテールノードが空の場合は、キューを初期化し、ヘッドノードを空のノードに設定します。ヘッドノードとは、現在実行中のノードを意味します。
5)チームのテールノードが空でない場合、CAS操作が実行され、現在のノードがチームのテールに追加されます。失敗した場合は、成功するまでスピンを続けます。
キューのノードエンキューロジックは非常に重要です。これは、スレッドがロックを取得および解放するプロセスを理解するために非常に重要なので、ここでもう一度要約します。
ノードがチームに入った後、ロック操作を取得する必要があります。acquireQueuedメソッドのソースコード分析は、次のように実行されます。
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
*
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 自旋,保证当前节点要么获取到了锁,要么获取锁失败后被阻塞了
for (;;) {
// 当前节点的前置节点
final Node p = node.predecessor();
// 如果前置节点为头节点,则此时再次尝试获取锁
if (p == head && tryAcquire(arg)) {
// 当前线程获取锁成功,则将当前节点设置为头节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 前置节点为非头节点或者再次获取锁失败后,执行线程挂起逻辑
// shouldParkAfterFailedAcquire 方法实现了判断获取锁失败后是否需要挂起
// parkAndCheckInterrupt 方法实现了线程挂起逻辑
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 线程挂起成功,返回 true
interrupted = true;
}
} finally {
// 线程被挂起了,取消其尝试获取锁
if (failed)
cancelAcquire(node);
}
}
AcquisitionQueuedメソッドは、主に次のロジックを実装します。
1)スピン戦略を使用して、現在のノードがロックを取得するか、ロックの取得に失敗した後に一時停止されるようにします
2)現在のノードのフロントノードがヘッドノードの場合は、ロックの取得を再試行してください
3)フロントノードがヘッドノードでない場合、またはロック取得が失敗した場合、スレッド一時停止ロジックが実行されます。
注:ヘッドノードは、現在ロックを保持しているスレッドを表します。現在のノードのpredノードがヘッドノードである場合、この時点でヘッドノードがロックを解放している可能性があるため、ロックの取得を試みる必要があります。再び。
次に、shouldParkAfterFailedAcquireメソッドを調べて、ロック取得が失敗した後のスレッドを一時停止する必要があるかどうかを判断します。ソースコードは次のとおりです。
/**
* Checks and updates status for a node that failed to acquire.
* Returns true if thread should block. This is the main signal
* control in all acquire loops. Requires that pred == node.prev.
*
* @param pred node's predecessor holding status
* @param node the node
* @return {@code true} if thread should block
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 前置节点的等待状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
// 如果 pred 节点为 SIGNAL 状态,返回true,说明当前节点需要挂起,此时 pred 节点准备获取锁了
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
// 如果 pred 节点为 CANCELLED 状态,则表示 pred 节点需要从队列中移除
do {
// 删除所有状态为 CANCELLED 的节点,重新定位当前节点的前置节点
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
// pred 节点是 PROPAGATE 或者 0,此时将 pred 节点状态统一设置为 SIGNAL,重新循环一次判断,即 acquireQueued 自旋方法
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
shouldParkAfterFailedAcquireメソッドは、主に次のロジックを実装します。
1)predノードのステータスがSIGNALの場合、trueを返し、現在のスレッドが一時停止ロジックを実行します
2)predノードのステータスがCANCELLEDの場合、ステータスがCANCELEDのすべてのノードを削除します
3)predノードの状態がPROPAGATEまたは0の場合、predノードの状態をSIGNALに設定し、acquireQueuedのスピンメソッドから再度判定を行います。
このプロセスは次のように要約されます。
predノードの状態に応じて現在のノードを一時停止できるかどうかを判断します。メソッドがfalseを返した場合、一時停止条件の準備ができていないため、acquireQueued(final Node node、int arg)のスピンボディに再入力し、再判断。trueが返された場合は、現在のスレッドがサスペンド操作を実行できることを意味し、その後、サスペンドを実行し続けます。
スレッド中断ロジックを実装するparkAndCheckInterruptメソッドを見てみましょう。ソースコードは次のとおりです。
/**
* Convenience method to park and then check if interrupted
*
* @return {@code true} if interrupted
*/
private final boolean parkAndCheckInterrupt() {
// 调用 UNSAFE 的 park 方法阻塞线程
LockSupport.park(this);
return Thread.interrupted();
}
この時点で、ロックを取得するプロセスは終了しました。ロジックは比較的複雑ではありません。ロックの解放の分析を開始しましょう。
3.3ロックを解除する
ロックを解除するためのロジックエントリはreleaseメソッドにあります。ソースコードは次のとおりです。
/**
* Releases in exclusive mode. Implemented by unblocking one or
* more threads if {@link #tryRelease} returns true.
* This method can be used to implement method {@link Lock#unlock}.
*
* @param arg the release argument. This value is conveyed to
* {@link #tryRelease} but is otherwise uninterpreted and
* can represent anything you like.
* @return the value returned from {@link #tryRelease}
*/
public final boolean release(int arg) {
// 释放锁,需要实现类自己实现逻辑
if (tryRelease(arg)) {
// 释放锁成功
Node h = head;
if (h != null && h.waitStatus != 0)
// 如果头节点不为空 && 头节点状态 != 0,则调用 unparkSuccessor 方法唤醒后继节点
unparkSuccessor(h);
return true;
}
return false;
}
releaseメソッドは、主に次のロジックを実装します。
1)、ロックを解除します。これには、実装クラスがビジネスを単独で実装する必要があります
2)ロックが正常に解放された後、ヘッドノードが空でない場合&&ヘッドノードステータス!= 0、unparkSuccessorメソッドを呼び出して後続のノードをウェイクアップします
unparkSuccessorメソッドを見てみましょう。ソースコードは次のとおりです。
/**
* Wakes up node's successor, if one exists.
*
* @param node the node
*/
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.
*/
// head 节点等待状态
int ws = node.waitStatus;
if (ws < 0)
// 如果 head 节点等待状态 小于 0 ,则将 head 节点重置为初始状态 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.
*/
// head 节点的后继节点
Node s = node.next;
if (s == null || s.waitStatus > 0) {
// 后继节点为空 或者 后继节点等待状态为 CANCELLED
s = null;
// 从尾结点开始向前遍历
for (Node t = tail; t != null && t != node; t = t.prev)
// 直到找到离头节点最近的等待状态小于等于 0 的节点
if (t.waitStatus <= 0)
// 找到的可以唤醒的节点赋值给 s
s = t;
}
if (s != null)
// 后继节点不为空,则直接调用 UNSAFE 的 unpark 方法唤醒后继节点对应的线程
LockSupport.unpark(s.thread);
}
unparkSuccessorメソッドは、主に次のロジックを実装します。
1)ヘッドノードの待機状態が0未満の場合は、ヘッドノードを初期状態0にリセットします。
2)ヘッドノードのサクセサノードを取得します。サクセサノードが空でない場合は&&サクセサノードの待機状態<= 0、UNSAFEのunparkメソッドを直接呼び出して、サクセサノードに対応するスレッドをウェイクアップします。
3)。サクセサノードが空の場合||サクセサノードの待機状態> 0の場合、テールノードから前方にすべてのノードをトラバースし、ウェイクアップできるヘッドノードに最も近いノードを見つけます(待機状態<= 0)。次に、手順2を実行します
ロック解除プロセスは次のように要約されます。
主にヘッドノードのサクセサノードをウェイクアップします。サクセサノードがウェイクアップ条件を満たす場合は、キューの最後から、条件を満たすヘッドノードに最も近いノードが見つかるまで待ちます。
4、まとめ
この記事では、主にAQSの内部構造とその同期実装原理について説明し、ソースコードの観点からAQS排他ロックモードでロックを取得および解放するロジックを詳細に分析し、ソースコードから次の結論を導き出します。 :
排他ロック内モードでは、状態値はロックを表すために使用され、0はロックフリー状態を表し、0から1はロックフリーからロック状態を表し、1つのスレッドのみがロック状態を保持できます。ロックすると、残りのスレッドはノードノードにカプセル化され、Suspendedを処理するためにキューに配置されます。キュー内のヘッドノードは、現在実行中のスレッドを表します。ヘッドノードが解放されると、後続のノードがウェイクアップされます。 AQSキューがFIFO同期キューであることの確認