バージョン: JDK 11
この記事のために、私はすでに廃棄しすぎています。私の能力は限られているので、はっきりと話すことは約束できません。
前提として、ジャッジはこれについて何か、特に CAS、キュー、スレッドのライフサイクルなどのいくつかの概念を知っていることが前提です。
また、これは純粋な AQS ソース コードの解析であり、実際のサブクラスと使用シナリオを組み合わせていないため、理解が困難です。後で、理解を深めるために AQS のサブクラスを分析します。
1.AQSとは?
AQS はAbstractQueuedSynchronizer
略語で、 Abstract Queue Synchronizerと訳されています。
名前が示すように、抽象化があるところには実装があり、抽象化の目的は非常に重要です。
- 抽象化: リソース取得の一般的なロジックを定義し、さまざまなシナリオで同期状態を維持するためにサブクラスを必要とします。
- キュー: 要求されたリソースが占有されている場合、要求元のスレッドには、ブロック、待機、およびウェイクアップのためのメカニズムが必要です。そしてこの仕組み、AQSはCLHキューで実装されており、ロックを取得できなかったスレッドを一時的にキューに入れます。
- シンクロナイザー: キューを見ない場合、実際には「抽象シンクロナイザー」です。つまり、同期ツールとロックの基本的なフレームワークが定義されており、AQS に基づいて、ReentrantLock、Semaphore などのカスタム同期ツールを簡単かつ効率的に構築できます。
2. AQSの原理
シンクロナイザーは通常、取得と解放の 2 つの操作を提供します。acquire は、同期状態で許可されるまで、または許可されない限り、呼び出し元のスレッドをブロックします。release は、acquire によってブロックされた 1 つ以上のスレッドが引き続き実行されるように同期状態を変更します。[1] の疑似コードに対応する:
acquire:
while (synchronization state does not allow acquire) {
enqueue current thread if not already queued;
possibly block current thread;
}
dequeue current thread if it was queued;
release:
update synchronization state;
if (state may permit a blocked thread to acquire)
unblock one or more queued threads;
AQS によって実装される一連のクラスは、実際には Lock.lock と unlock、Semaphore.acquire と release などの抽象的な概念を統合する 2 つの操作に基づいています。
簡単に言えば、上記のプロセスを実装する必要がある場合は、AQS が少なくとも提供する必要がある機能と、それらの間のコラボレーションに戻ります。
- 同期状態のアトミック管理
- スレッドのブロックとブロック解除
- ブロッキング スレッドのキューイング
これらの機能に基づいて、AQS は以下を拡張します。
- 中断することができます。
- タイムアウト制御;
- モニターの形式で待機/シグナル操作をサポートするために使用される条件。
シンクロナイザーには、同期状態を制御するための 2 つの実装 (または 2 つの管理方法) があります。排他モードと共有モードです。
3.コア構造
3.1 同期ステータス
同期状態を管理することで、取得可能な共有リソースを特定することができます。
複数のスレッドが戻って同期状態を操作しようとする可能性があるため、揮発性と CAS という 2 つのポイントがあります。
リソースの数やロックの状態を表すなど、状態の特定の意味がサブクラスによって定義および操作されるため、操作メソッドが保護されていることがわかります。
- ReentrantLock のサブクラス Sync の場合、state はスレッドがロックされた回数を示します (0 はロックされていないことを意味し、それ以外の場合は、同じ再入可能スレッドによってロックされた回数を意味します)。
- ReentrantReadWriteLock の場合、状態の上位 16 ビットはスレッドが読み取りロックをロックした回数を表し、下位 16 ビットはスレッドが書き込みロックをロックした回数を表します (読み取りロックと書き込みロックも再入可能であり、相互反発になります)
- Semaphore の場合、state は使用可能なセマフォを表します. 簡潔で大まかなステートメントは次のとおりです: 取得できるリソースの数を表します.
- CountDownLatch の場合、状態はラッチがまだ保持している「ロック」の数を示します (本当の意味でのロックであり、コード内の Lock または synchronized キーワードでは表されません)。複数のスレッドが実行された後、状態が 0 になることがわかります。ロックを解除し、ラッチが開いていることをマークします (対応するブロックされたスレッドは、この時点で実行のために解放することしかできません)。
/**
* The synchronization state.
*/
private volatile int state;
// 返回同步状态的当前值。
protected final int getState() {
return state;
}
// 设置同步状态的值。
protected final void setState(int newState) {
state = newState;
}
// 如果当前状态值等于期望值,则以原子方式将同步状态设置为给定的更新值。
protected final boolean compareAndSetState(int expect, int update) {
return STATE.compareAndSet(this, expect, update);
}
3.2 スレッドのブロックとウェイクアップ
スレッドのブロックとウェイクアップは、主に LockSupport クラスによって操作されます。このクラスは Unsafe# パーク & アンパークのカプセル化の層であり、これに基づいて、タイムアウト メソッドが許可されます。
park は、パーク解除されるまで、またはパーク解除されない限り、スレッドをブロックします。
- park を呼び出す前に unpark を呼び出すと、park は役に立ちません。
- unpark の呼び出しはカウントされないため、par 呼び出しの前に unpark メソッドを複数回呼び出すと、パーク操作が取り消されるだけです。
3.3 排他モードでのスレッド節約
Node に関連付けられたスレッドは、リソースが占有されているときに待機しているスレッドを識別するため、リソースを既に占有しているスレッドが存在する必要があります。そして、AQS の親クラスがこの役割AbstractOwnableSynchronizer
を果たします。ソース コードは非常に単純で、この排他的なスレッドを維持するだけです。
public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {
protected AbstractOwnableSynchronizer() {
}
private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}
3.4 CLH キューのバリアント
CLH ロックは、Craig, Landin, and Hagersten (CLH) ロックです。これは、基礎となるレイヤーがキューに基づいて実装されているため (つまり、厳密な先入れ先出し)、一般に CLH キュー ロックと呼ばれます。CLH ロックもリンクド リストに基づくスケーラブルで高性能なロックであり、一般的にスピン ロックに使用されます。
最初の変更 - ノードのリンク
CLH ロックのノードは、pred によって先行ノードの位置を維持し (オリジナル バージョンにはありませんが、スピン ロック アプリケーション シナリオにはあります)、spin によって先行ノードの状態を判断して現在のノードを判断します。
キューに入る場合は、競争と操作を判断するためにテールのみに基づいています。それ以外の場合は、ヘッドのみに基づいています。
+------+ prev +-----+ +-----+
head | | <---- | | <---- | | tail
+------+ +-----+ +-----+
この設計の利点は、ノード間で関連付けられていない通常のキューとは異なり、AQS が CLH ロックを使用して「タイムアウト」と「キャンセル」を処理できることです。先行ノードがタイムアウトまたはキャンセルすると、pred を介して転送できます」ノードリンクでキャンセルされました。(元の目的は、前のノードに移動してその状態フィールドを使用することです。)
CLHロックが前のノードの状態を変更すると、次のノードはスピンに基づいてそれを認識しなければなりません。しかし、AQS では、次のノード (スレッド) を明示的にウェイクアップするために、バックドライブ ノードを次に維持する必要もあります。
next が空の場合 (おそらくキャンセルされた場合)、実際の次のノードがあるかどうかを確認するために、tail から楽しみにします。
第 2 のバリエーション - ノードのプロパティ
元のノード属性はスピンの判断に使用され、AQS で最も重要なスレッドのブロックとウェイクアップはノード間の通信です。したがって、排他的および共有モードでのノード間の通信に使用されるノードの状態がより豊富になることが必要であり、場合によっては条件の場合にも使用されます。
4 CLH キューバリアント構造
4.1 キュー構造
[外部リンクの画像転送に失敗しました。ソース サイトにはアンチリーチング メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-V111JxLl-1616247740859)(/Users/jry/Downloads/code-snapshot (3) ].png)
ノードは、元の CLH キューとはかなり異なる CLH キューの構造を定義します。
-
waitStatus :現在の(元の CLH との最大の違い) ノードのステータスを特定し、設定するすべてのステータス定数を一覧表示します。
状態間のフローは後で詳しく分析しますが、特に負の値の状態はスレッド間の通信を伴います。
-
prev, next: 先行ノードと後続ノード。
-
スレッド: ノードは実際には「待機中」のスレッドです。
-
SHARED、EXCLUSIVE: 所有モード (共有または排他) を表すために使用されるノード インスタンス。nextWaiter で設定できます。
-
nextWaiter: 条件で次に待機しているノードを示します。または、SHARED は共有を識別します。当然、待ち時間はありません。
値がない場合は、排他モードを識別するのが最も簡単です。
-
CLHはもともと仮想ヘッドノードを持っていましたが、AQSではヘッドノードは最初の競合まで遅延されます(同様にテールノードもあるが、最初はヘッドとテールは同じです)。
重要な変数操作ハンドルと対応する CAS 操作がまだあります。
// 其实底层用的还是 Unsafe
private static final VarHandle NEXT;
private static final VarHandle PREV;
private static final VarHandle THREAD;
private static final VarHandle WAITSTATUS;
static {
try {
MethodHandles.Lookup l = MethodHandles.lookup();
NEXT = l.findVarHandle(Node.class, "next", Node.class);
PREV = l.findVarHandle(Node.class, "prev", Node.class);
THREAD = l.findVarHandle(Node.class, "thread", Thread.class);
WAITSTATUS = l.findVarHandle(Node.class, "waitStatus", int.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
結局、AQS の全体的なデータ構造には、実際には 2 つのキューがあります。同期待機キュー(コードで言及されている名前はSyncQueue ) と、この構造に基づいて実装された条件付きキュー(nextWaiter: ConditionQueue )です。簡単に言えば、次のようになります。
(画像ソース: https://blog.csdn.net/y4x5M0nivSrJaY3X92c/article/details/112645551)
4.2 同期キュー
SyncQueue は、状態リソースを待機しているブロッキング キューです。これは、AQS が処理を担当するフィールドです。これは、ブロッキング スレッドをノードとして使用し、スレッドのブロッキング、覚醒、伝播をエンキューおよびデキュー操作に関連付ける一連のキューを抽象化および標準化します。
// 延迟初始化;或者通过 setHead() 设置
// 状态一定不会是 CANCELLED
// 头结点是虚拟结点, 所以一定不会关联线程
private transient volatile Node head;
// 延迟初始化;只能通过 enq() 添加新的结点
private transient volatile Node tail;
// 用于结点出队, 所以这个节点就没用了, 用来设为 head
// 仅通过acquire方法调用, 成功就是获取到资源了, 不用排队了
// 所以对应到上文中, 头结点的状态不会是 CANCELLED
private void setHead(Node node) {
head = node;
// 自然就不关联线程了
node.thread = null;
node.prev = null;
}
// VarHandle mechanics
private static final VarHandle STATE;
private static final VarHandle HEAD;
private static final VarHandle TAIL;
static {
try {
MethodHandles.Lookup l = MethodHandles.lookup();
STATE = l.findVarHandle(AbstractQueuedLongSynchronizer.class, "state", long.class);
HEAD = l.findVarHandle(AbstractQueuedLongSynchronizer.class, "head", Node.class);
TAIL = l.findVarHandle(AbstractQueuedLongSynchronizer.class, "tail", Node.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
// Reduce the risk of rare disastrous classloading in first call to
// LockSupport.park: https://bugs.openjdk.java.net/browse/JDK-8074773
Class<?> ensureLoaded = LockSupport.class;
}
private final void initializeSyncQueue() {
Node h;
// 设置虚拟头结点, 头尾相同
if (HEAD.compareAndSet(this, null, (h = new Node())))
tail = h;
}
// CAS 设置 tail
private final boolean compareAndSetTail(Node expect, Node update) {
return TAIL.compareAndSet(this, expect, update);
}
ノードがエンキューされると、それは占有されているか使い果たされていることを意味します。取得を試み続けるスレッドはキューに入れられ、キューに参加するときに、キューが初期化されていない場合は、最初にキューが初期化されます (主にヘッドが仮想ノードであるため)。
// 结点入队; 首次则初始化
// 主要用于 ConditionQueue 中
private Node enq(Node node) {
// 循环 + CAS
for (;;) {
Node oldTail = tail;
if (oldTail != null) {
node.setPrevRelaxed(oldTail);
// 尾结点的设置可能存在竞争, 所以需要 CAS
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
return oldTail;
}
} else {
// 相当于头尾皆空, 第一次入队
initializeSyncQueue();
}
}
}
// 主要用于 SyncQueue 的入队
private Node addWaiter(Node mode) {
// 不同于 enq, node 代表是预设的 EXCLUSIVE 或 SHARED.
// 标识"独占" or "共享"
Node node = new Node(mode);
for (;;) {
Node oldTail = tail;
if (oldTail != null) {
node.setPrevRelaxed(oldTail);
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
return node;
}
} else {
initializeSyncQueue();
}
}
}
// 用于结点出队, 所以这个节点就没用了, 用来设为 head
// 仅通过acquire方法调用, 成功就是获取到资源了, 不用排队了
// 所以对应到上文中, 头结点的状态不会是 CANCELLED
private void setHead(Node node) {
head = node;
// 自然就不关联线程了
node.thread = null;
node.prev = null;
}
エンキュー:
デキュー:
[外部リンクの画像転送に失敗しました。ソース サイトには盗難防止リンク メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-XotB6UpU-1616247740869)(https://gitee.com/ryan_july/clouding_img/生/マスター/img/21312312413123 .gif)]
4.3 条件キュー
ConditionQueue は、AQS の比較的特殊で複雑なキューです。AQS でリソースのスクランブリングとスレッド通信のみを処理する SyncQueue と比較して、ConditionQueue は、開発者がスレッドを手動で操作できるように、Object#wait、notify、notifyAll に一致するように設計されたデータ構造です。
ConditionQueue は SyncQueue に基づいているか、Node 構造に基づいています。そしてその運用の入り口、JUCと接してきた学生なら知っておくべき、それがCondition
インターフェース。
AQS に関連するメソッドはすべてそこにありConditionObject
、このクラスはConditon
インターフェイス。
Conditionの待ち方の中で、「誤起床」が繰り返し出てきます。ループ内で待機メソッドを使用し、起床後に条件成立か再判断することをお勧めします。
public interface Condition {
// 当前线程进入等待状态直到被唤醒或者中断
void await() throws InterruptedException;
// 当前线程进入等待状态,不响应中断,阻塞直到被唤醒
void awaitUninterruptibly();
// 当前线程进入等待状态直到被唤醒或者中断,阻塞带时间限制
long awaitNanos(long nanosTimeout) throws InterruptedException;
// 当前线程进入等待状态直到被唤醒或者中断,阻塞带时间限制
boolean await(long time, TimeUnit unit) throws InterruptedException;
// 当前线程进入等待状态直到被唤醒或者中断,阻塞带时间限制
boolean awaitUntil(Date deadline) throws InterruptedException;
// 唤醒单个阻塞线程
void signal();
// 唤醒所有阻塞线程
void signalAll();
}
Condition
ConditionObject
現在、 JUCで使われてCondtion
いる実装クラスは AQS だけと言えます。ソースコードを見てみましょうConditionObject
. SyncQueueと同様に、キューの構造を理解しやすくするために、とりあえずキューに入る方法と出る方法だけに焦点を当てます.
public class ConditionObject implements Condition, java.io.Serializable {
private transient Node firstWaiter;
private transient Node lastWaiter;
public ConditionObject() {
}
// 每个 await 方法第一步就是调用该方法加入队列
private Node addConditionWaiter() {
if (!isHeldExclusively())
// 只有独占模式下,才有 Condition 的作用
throw new IllegalMonitorStateException();
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
// 在 ConditionQueue 中,只要不是 CONDITION 状态, 都看做取消等待了.需要清除出去
if (t != null && t.waitStatus != Node.CONDITION) {
// 遍历清除"取消"结点
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
// 一般都是在等待期间进行取消
// 1.插入新结点发现 lastWaiter 是取消的
// 2.线程被唤醒时, 如果后面还有等待的结点,就做一次处理
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
}
コア構造が完成した今、すべての AQS 操作はこのようなシステムに基づいています。実際、全体的な観点からは、おそらくコンテキストを把握しています。
- 1. リソースを取得し、状態を操作します。
- 2. リソースが占有されている
- 2.1排他モードでは、スレッドは待機する必要があり、スレッドはノードとしてパッケージ化され、キューに入れられます。
- 初めてチームに参加するときは、最初にヘッド仮想ノードを構築する必要があります。
- ノードのエンキューをブロックしています。
- 2.2 共有モードでは、状態を操作し続けることができる場合があります。
- 2.1排他モードでは、スレッドは待機する必要があり、スレッドはノードとしてパッケージ化され、キューに入れられます。
- 3. リソースを競う方法は?
- 4.もうつかめないのですが、どうすればやめられますか?
- 5. リソースが解放されました。キューに入れられたフォローアップ ノードを起動して取得するにはどうすればよいですか?
- 6. 急いで逃げることができない人は、他の人の列に影響を与えますか?
最初にリソースを取得して状態を操作する方法を見てみましょう
5. 運用リソースのテンプレート API
AQS は、シンクロナイザーの標準プロセスと操作を定義する抽象フレームワークにすぎないため、サブクラスが拡張機能を実装するための残りのポイントは、状態と対応する操作 (AQS の実装に基づく取得と解放) に関連する実際の意味を与えることです。状態の CAS メソッドの) (排他的および共有を含む)。
isHeldExclusively()//该线程是否正在独占资源。只有用到 condition (AQS.ConditionObject)才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
これらのメソッドは保護されており、メソッドはすべてthrow new UnsupportedOperationException()
. サブクラスは、状態の CAS メソッドを使用してこれらのメソッドを実装し、実際のアプリケーション シナリオで状態に意味を与える必要があります。
ps: 他のメソッドは、private または final のいずれかです。
もちろん、それらすべてを実装する必要はなく、依然としてサブクラスのシナリオに基づいています。例えば、排他モードでのみ使用する場合は、共有モードなどのメソッドを実装する必要はなく、同様に、共有ReentrantLock
モードなどの排他メソッドを実装する必要もありませんSemaphore
。
ただし、すべての実装に限定されるわけではなく、たとえばReentrantReadWriteLock
、書き込みロックは排他的であり、読み取りロックは共有されます。
ReentrantLock を例にとると、状態は 0 に初期化され、ロックされていない状態を示します。A スレッド lock() の場合、tryAcquire() を呼び出してロックを独占し、state+1 します。その後、他のスレッドは tryAcquire() で失敗し、A スレッド unlock() が state=0 に達する (つまり、ロックが解放される) まで、他のスレッドはロックを取得する機会があります。ロックは再入可能であり、スレッド A はこのロックを繰り返し取得できます (状態は蓄積されます)。ただし、状態が確実にゼロに戻るように、何回リリースする必要があるかに注意してください。
CountDownLatch を例にとると、タスクは N 個のサブスレッドに分割されて実行され、状態も N に初期化されます (N はスレッドの数と一致している必要があることに注意してください)。メインスレッドは await() (tryAcquireShared()) を呼び出してブロックします。各子スレッドが実行された後、countDown() (releaseShared(1)) が 1 回実行され、CAS で状態が 1 減少します。すべてのサブスレッドが実行されると (つまり、状態 = 0)、メイン スレッドは unpark() になり、メイン スレッドは await() 関数から戻り、残りのアクションを続行します。
6. リソースの取得と解放
AQS 自体のパブリック メソッドは制限されており、取得と解放に関連するメソッドのみがリソース操作に関与し、タイムアウトと中断が区別されます。
理論的には、サブクラスに特別な事情がなければ、テンプレート メソッドが正しく定義されている限り、ユーザーは AQS のパブリック メソッドを直接使用して、実際に同時実行ツールを使用できます。たとえば、ReentrantLock
lock、tryLock、および release はすべて、AQS を直接呼び出すパブリック メソッドです。
public void lock() {
sync.acquire(1);}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.release(1);}
取得または解放のいずれであっても、排他的モードと共有モードの 2 つのモードがあり、これについては前述のとおりです。
上記の 2 つの下位レベルの操作、リソース操作 (サブクラスの実装) とスレッド エンキューについては既に説明しました。
次に、プロセスを区別してプロセス全体を整理し、スレッドの起床、スレッドの通信、リソースの競合、状態の転送などを含む AQS のソース コードを確認する必要があります。
6.1 独占取得
6.1.1 プロセスの概要
最初に結論を言いましょう。つまり、写真を見てください。私は深くではなく、幅広く探していることに注意してください。これらの方法はすべて類似しており、基本的に同じであるため、最初に 1 つのレベルで方法を分析してから、段階的に進めてください。
6.1.2 公募方法
// 1.独占获取,忽略中断.
// arg 的含义取决于 state 的含义
public final void acquire(int arg) {
// a.首先尝试获取资源
if (!tryAcquire(arg) &&
// b.失败, 标志为独占结点, 进入等待队列
// c.继续循环尝试获取资源(头结点) || 阻塞,等待前驱结点唤醒
// c-d.异常的情况下, 可能需要取消排队, 唤醒后继结点, 恢复中断
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
// e.如果原来是中断的, 则恢复中断
selfInterrupt();
}
}
// 2.可被中断的独占获取
public final void acquireInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
// 因为响应中断,所以不用恢复中断
// 和 acquire 中的 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 是一样的效果,只是阻塞时响应中断
doAcquireInterruptibly(arg);
// 异常的情况下, 可能需要取消排队, 唤醒后继结点
}
// 3.可被中断的超时独占获取
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
// 和 acquire 中的 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 是一样的效果
// 不同的是 1.阻塞时响应中断; 2.超时阻塞、循环获取资源时,首先判断时间允许。否则需要取消排队, 唤醒后继结点
doAcquireNanos(arg, nanosTimeout);
// 异常的情况下, 可能需要取消排队, 唤醒后继结点
}
最外層は非常に単純で、1 つは割り込みを判断して処理する層で、もう 1 つは最初にリソースを取得しようとする層です。
ただし、取得が失敗すると、後続のキュー操作が必要になります (ノードとしてのパッケージ化、ブロック、ウェイクアップ、キャンセル、先行ノードの処理など)。
acquireQueued + addWaiter
、doAcquireInterruptibly(arg)
、doAcquireNanos
および はまさにこの種のことを行いますが、タイムアウトとブロッキングの処理は異なります。
6.1.3 無限ループの取得、キューイング、ブロッキング
最初のタイプacquireQueued
(残りは似ています) を見てみましょう。これは、上の図のフレーム領域です。
final boolean acquireQueued(final Node node, int arg) {
// 用来标记原始中断状态
boolean interrupted = false;
try {
for (;;) {
final Node p = node.predecessor();
// 每次循环判断前驱结点是否 head,
// 是,则说明前面无等待的线程, 尝试获取资源
if (p == head && tryAcquire(arg)) {
// 获取成功, 出队
setHead(node);
p.next = null; // help GC
return interrupted;
}
// 判断是否阻塞
if (shouldParkAfterFailedAcquire(p, node))
// 阻塞, 并拿到原始的中断状态
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
// 取消排队等待
cancelAcquire(node);
if (interrupted)
// 恢复中断
selfInterrupt();
throw t;
}
}
// 所有类型的获取方法,在循环中都依据该方法,进行阻塞判断
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 而当前线程要阻塞的前提, 就是要找个小伙伴到时候叫醒他. 而这个小伙伴,通常就是前驱结点
// 怎么让小伙伴到时候记得提醒自己, 就是将它的状态设为 SIGNAL, 表示到时候叫醒我(如何叫醒见 unparkSuccessor)
return true;
if (ws > 0) {
// 表示前驱结点是取消状态, 往前把取消的都清理掉
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 前驱结点可能正在获取, 所以这里先设置状态, 但不阻塞, 再尝试一次
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
return false;
}
// 阻塞当前线程,获取并且重置线程的中断标记位
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
他の 2 つのタイプには、割り込み処理またはタイムアウト処理が追加されています。
private void doAcquireInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return;
}
// 这里是要响应中断的.
if (shouldParkAfterFailedAcquire(p, node) &&
// 被唤醒之后马上检查中断状态, 并尝试恢复
parkAndCheckInterrupt())
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
// 先检查超时
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return true;
}
nanosTimeout = deadline - System.nanoTime();
// 每次循环(唤醒)都检查超时
if (nanosTimeout <= 0L) {
cancelAcquire(node);
return false;
}
if (shouldParkAfterFailedAcquire(p, node) &&
// 如果超时剩余时间小于 1000 ns, 就继续循环.避免线程状态切换的损耗
nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
6.1.4 キャンセルキュー
例外が発生した場合、中断が発生した場合、またはタイムアウトが発生した場合は、以前にaddWaiter
追加した(すぐに削除されない可能性があり、他のノードへの影響を避けるために途中で「キャンセル ステータス」が使用されます)。待って。
次に、現在のノードの状況に応じて何らかの処理を行う必要があります。
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
// 1.置空节点持有的线程,因为此时节点线程已经发生中断
node.thread = null;
// 2.跳过已取消的前驱结点, 找个第一个有效的前驱
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next;
// 3.将状态设为取消
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {
// 4.1如果是 tail, 重新设置 tail, 并且将前驱的 next 置空
pred.compareAndSetNext(predNext, null);
} else {
int ws;
// 4.2 既不是 tail, 也不是 head.next
// 则将前继节点的waitStatus置为SIGNAL(因为后面肯定还有等待的)
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && pred.compareAndSetWaitStatus(ws, Node.SIGNAL))) &&
// 注意 原来的状态不能是取消, 或者等待取消(thread == null)
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
// 并使node的前继节点指向node的后继节点
pred.compareAndSetNext(predNext, next);
} else {
// 4.3 如果node是head的后继节点,则直接唤醒node的后继节点
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
6.2 独占リリース
比較的言えば、独占リリースは単なるテンプレートのセットであり、ロジックは明確です
- リソースの解放(確かにスレッドを保持するかどうかの判断が含まれますが、それはサブクラスによって行われます)
- 頭の状態をリセット
- ヘッドの有効なサクセサ ノードのスレッドをウェイクアップする
// 独占释放
public final boolean release(int arg) {
// 1.首先释放资源
if (tryRelease(arg)) {
Node h = head;
// 2.头结点不为空, 并且是阻塞的情况(需要唤醒后继的状态)
if (h != null && h.waitStatus != 0)
// 3.唤醒后继结点(跳过中间可能存在取消的结点)
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
// 重置为 0. 后面结点唤醒后会成为新的 head
node.compareAndSetWaitStatus(ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
// 如果是空的,或者取消,则从后往前找到第一个等待唤醒的:
// 1. addWaiter(Node node) 是先设置前驱节点 后设置后继节点 虽然这两步分别是原子的 但在两步之间还是可能存在后继节点未链接完成的情况
// 2. 在产生CANCELLED状态节点的时候,先断开的是Next指针,Prev指针并未断开,因此也是必须要从后往前遍历才能够遍历完全部的Node
// 如果是从前往后找,由于极端情况下入队的非原子操作和CANCELLED节点产生过程中断开Next指针的操作,可能会导致无法遍历所有的节点
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
if (s != null)
// 被唤醒, 结合前文, 就是退出 parkAndCheckInterrupt, 重新循环
LockSupport.unpark(s.thread);
}
6.3 共同取得
6.3.1 概要
共有モードでの取得と解放は、排他モードでのリソース操作とデキューエントリと同じですが、違いは (下図の赤いフォントのプロセスノード)
[外部リンクの画像転送に失敗しました。ソース サイトには盗難防止リンク機構がある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-28VCQeZm-1616247740874)(/Volumes/personal/note/AQS-copy of page 1.png)]
6.3.2 公募方式
// 忽略中断的共享获取
// 最后恢复中断
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
// 获取失败, 结点入队, 循环尝试或阻塞
doAcquireShared(arg);
}
// 可被中断的共享获取
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
// 同 acquireShared, 循环或阻塞过程检查中断
doAcquireSharedInterruptibly(arg);
}
// 可被中断的超时共享获取
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquireShared(arg) >= 0 ||
// 同 acquireShared, 循环或阻塞过程检查中断以及是否超时
doAcquireSharedNanos(arg, nanosTimeout);
}
6.3.3 ループ取得、キューイング、ブロッキング
上の図から、赤いフォントで示されているプロセスが 3 つしかないことがはっきりとわかります。
- ノードはキューに入れられ、モードは共有されます。
- 取得が成功すると、残りの使用可能なリソースが返されます。現在のスレッドは、直接終了するのではなく、後続ノードを順番にウェイクアップする必要があります。
- 解放されると、後続ノードが同時にバッチで起動されます。
doAcquireShared
したがって、例として取り上げます。
private void doAcquireShared(int arg) {
// 共享模式
final Node node = addWaiter(Node.SHARED);
boolean interrupted = false;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
=================================
| if (r >= 0) {
|
| // 设置头结点, 并传播都后继结点 |
| setHeadAndPropagate(node, r); |
| p.next = null; // help GC |
| return; |
=================================
}
}
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
} finally {
if (interrupted)
selfInterrupt();
}
}
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
// h.waitStatus 是同时处理 SIGNAL 和 PROPAGATE, 结合 doReleaseShared 设置 PROPAGATE.
if (propagate > 0
// 应该是 0 的(当前被唤醒, 或直接拿到)
// 如果不是 0, 则应该是 PROPAGATE. 说明 setHead 完成之前, 其他线程释放资源,然后将老的 head 改为 PROPAGATE.见 doReleaseShared
|| h == null || h.waitStatus < 0 ||
// 新的 head 可能有三种情况
// 1. 0 :没有后继结点或刚入队,没来得及改头, 见 shouldParkAfterFailedAcquire
// 2. -1 : 已入队, 并将 head 改为 SIGNAL , 见 shouldParkAfterFailedAcquire
// 2. -3 : 其他线程释放资源, 将 head 还为 0 时, 改为 PROPAGATE. 见 doReleaseShared
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
continue; // loop to recheck cases
// 如果是需要信号的结点, 则直接尝试唤醒
unparkSuccessor(h);
}
else if (ws == 0 &&
!h.compareAndSetWaitStatus(0, Node.PROPAGATE))
// 否则设置为传播
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
// 可能其他线程拿到资源出队了, 所以继续循环
break;
}
}
PROPAGATE の状態が何に役立つのかずっと疑問に思っていました.doReleaseShared と setHeadAndPropagate を組み合わせることで、主に同時発生状況での後続のウェイクアップの問題を解決します。
doReleaseShared が PROPAGATE を設定しない場合、setHeadAndPropagate は 0 と -1 のみを判断します。
それで、結果はどうでしたか?記事を参照してください: https://juejin.cn/post/6910104545133920269#heading-17
6.4 共有リリース
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
// 见上文
doReleaseShared();
return true;
}
return false;
}
ここまでで、AQS で取得、キューイング、ブロック、ウェイクアップ、リリースという非常に困難なプロセスをようやく完了しました。
7. キューチェック方法
7.1 Open メソッド public final
boolean hasQueuedThreads();//是否有等待线程
Collection<Thread> getExclusiveQueuedThreads();//获取独占等待线程列表
boolean hasContended() {
return head != null;}// 是否有线程争抢过资源
Thread getFirstQueuedThread();//第一个等待线程
boolean isQueued(Thread thread);//指定线程是否在排队
boolean hasQueuedPredecessors()//当前线程前是否还有其他排队的线程,一般用作公平锁的实现中
int getQueueLength()// 排队长度,以 thread 不为空 为准
Collection<Thread> getQueuedThreads()//排队线程集合
Collection<Thread> getExclusiveQueuedThreads()//独占排队线程集合
Collection<Thread> getSharedQueuedThreads()//共享排队线程集合
7.2 非公開メソッド
//获取第一个等待线程,区别于公有方法直接使用 head == tail 判断失败,则使用该方法。
// 先从前往后,找不到则从后往前。避免并发影响
private Thread fullGetFirstQueuedThread();
final boolean apparentlyFirstQueuedIsExclusive()//是否存在第一个排队的线程是以独占模式。读写锁非公平实现中获取读锁时先判断是否有在等待写锁
ここまでで、AQS Sync Queue 処理の方法も終わりました。残りの Node 操作メソッドは Condition Queue 用に設計されており、後で別の記事とサブクラスの実装があります。
8. フレームワークメソッドのまとめ
これは Meituan の技術チームの写真です. 一目で明らかです. 車輪を再発明する必要はないと思います.
画像ソース: https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html
9.排他モードの実装例
public class MyLock {
class Sync extends AbstractQueuedSynchronizer {
protected boolean tryAcquire(int arg) {
return compareAndSetState(0, 1);
}
protected boolean tryRelease(int arg) {
setState(0);
return true;
}
protected boolean isHeldExclusively() {
// 主要是 condition用到, 随便意思一下
return true;
}
}
Sync sync = new Sync();
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
}
このツールで例を実行する
public class AqsDemo {
static int lockCount = 0;
static int unlockCount = 0;
static MyLock myLock = new MyLock();
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run () {
try {
for (int i = 0; i < 10000; i++) {
unlockCount++;
}
// 睡一会,确保会有线程切换
Thread.sleep(1000);
myLock.lock();
System.out.println(Thread.currentThread().getName() + ":我开始跑了");
for (int i = 0; i < 10000; i++) {
lockCount++;
}
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + ":我跑完了");
} catch (Exception e) {
e.printStackTrace();
} finally {
myLock.unlock();
}
}
};
Thread thread1 = new Thread(runnable, "线程1");
Thread thread2 = new Thread(runnable, "线程2");
thread1.start();thread2.start();thread1.join();thread2.join();
System.out.println("加锁累加: " + lockCount);
System.out.println("未加锁累加: " + unlockCount);
}
}
結果:
线程1:我开始跑了
线程1:我跑完了 // 即使中间睡了一秒钟,也不会切换,线程是持有锁的
线程2:我开始跑了
线程2:我跑完了
加锁累加: 20000
未加锁累加: 14667
要約する
ソースコードはたくさんありますが、どちらかというと退屈で、理解しやすくて無能かもしれません。主に私自身のコンテキストとソースコードを見る習慣から来ているので、皆様の参考になれば幸いです。
これは私が自分で書く前の文脈であり、あまり気分が良くありません。長すぎるので、サブクラス実装ツールの説明と使い方は書ききれませんでした
巨人の肩
- java.util.concurrent Synchronizer Framework の中国語翻訳
- http://www.throwable.club/2019/04/07/java-juc-aqs-source-code/
- https://snailclimb.gitee.io/javaguide/#/docs/java/multi-thread/AQS%E5%8E%9F%E7%90%86%E4%BB%A5%E5%8F%8AAQS%E5% 90%8C%E6%AD%A5%E7%BB%84%E4%BB%B6%E6%80%BB%E7%BB%93?id=_23-aqs-%e5%ba%95%e5%b1 %82%e4%bd%bf%e7%94%a8%e4%ba%86%e6%a8%a1%e6%9d%bf%e6%96%b9%e6%b3%95%e6%a8%a1 %e5%bc%8f
- https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html