AQS 10,000ワードのグラフィックとテキストの包括的な分析
序文
同時実行性に関して言えば、AQS(AbstractQueuedSynchronizer)
いわゆるAQS
抽象キュー シンクロナイザーには内部的にロック関連のメソッドが多数定義されており、よく知られている 、ReentrantLock
、ReentrantReadWriteLock
などCountDownLatch
はSemaphore
すべてAQS
に基づいて実装されています。
まずはAQS
関連する写真を見てみましょうUML
。
マインドマッピング:
AQS実装原理
AQS
volatile int state
1 つ(共有リソースを表す) とFIFO
スレッド待機キューが維持されます(このキューは、マルチスレッドがリソースを求めて競合し、ブロックされたときに入力されます)。
これによりvolatile
、マルチスレッドでの可視性を確保することができますstate=1
現在のオブジェクトのロックが占有されている場合、他のスレッドはロックに失敗します ロックに失敗したスレッドは待ちキューに入れられ、比率が演算されFIFO
ますUNSAFE.park()
サスペンド、ロックを取得した他のスレッドがロックを解放するのを待ってからウェイクアップされます。
他の操作は、同時変更の安全性を確保するstate
ために実行されます。CAS
具体的な原理を図で簡単に要約すると、次のようになります。
AQS
ロックの多くの実装メソッドを提供します。
-
getState(): ロックのフラグ状態値を取得します。
-
setState(): ロックフラグの状態値を設定します。
-
tryAcquire(int): 排他的にロックを取得します。リソースの取得を試行し、成功した場合は true を返し、失敗した場合は false を返します。
-
tryRelease(int): 排他的にロックを解放します。リソースの解放を試行し、成功した場合は true を返し、失敗した場合は false を返します。
ここに挙げていないメソッドもまだいくつかありますが、次にReentrantLock
ソースコードと図面をブレークスルーポイントとして、AQS
内部の実装原理を段階的に理解していきます。
ディレクトリ構造
AQS
この記事では、ソース コードを分析するために、マルチスレッドがロックを競合し、ロックを解放するシナリオをシミュレートします。
3 つのスレッド (スレッド 1、スレッド 2、およびスレッド 3) が同時にロック/ロックを解放します。
ディレクトリは次のとおりです。
-
スレッドが正常にロックされたときに
AQS
内部的に実装されます -
スレッド2/3がロック失敗時の
AQS
待機キューのデータモデル -
スレッド 1 がロックを解放し、スレッド 2 がロックを取得する実装原理
-
スレッドシナリオによるフェアロックの具体的な実装原則を説明する
-
スレッド シナリオを通じて、Condition 内の
wait()
andsignal()
の実装原理を説明する
AQS
ここではロックとロック解除後の各スレッドの内部データ構造と実装原理を解析する図を描いていきます。
シナリオ分析
スレッド 1 は正常にロックされました
3 つのスレッドが同時にロックを取得した場合、スレッド 1 はロックの取得に成功しますが、スレッド 2と3 はロックの取得に失敗します。具体的な実行プロセスは次のとおりです。
この時点の内部データはAQS
次のとおりです。
スレッド 2とスレッド 3 はロックに失敗しました:
図からわかるように、待機キュー内のノードはNode
双方向リンク リストです。ここに の属性があり、SIGNAL
真ん中Node
に別の属性があります。これは図には描かれていません。これについては、で説明します。詳細は後ほど。waitStatus
Node
nextWaiter
Condition
プリエンプション ロック コードの実装を詳しく見てみましょう。
java.util.concurrent.locks.ReentrantLock .NonfairSync:
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
ここで使用されるReentrantLock は、不公平なロックです。スレッドが入ってきて、CAS
ロックを直接プリエンプトしようとします。プリエンプションが成功すると、state
値は 1 に変更され、オブジェクトの排他ロック スレッドが現在のスレッドに設定されます。次のように:
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
スレッド 2 がロックを取得できませんでした
実際のシナリオに従って分析してみましょう。スレッド 1 がロックを正常に取得した後、スレッドstate
1 は 1 に変更されます。スレッド 2 は、変数CAS
を変更すると必然的に失敗します。state
このときAQS
、FIFO
キュー内のデータ (先入れ先出し) は次の図のようになります。
スレッド2 の実行ロジックを段階的に分解してみましょう。
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire()
:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
まずtryAcquire()
具体的な実装を見てみましょう : java.util.concurrent.locks.ReentrantLock .nonfairTryAcquire()
:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
nonfairTryAcquire()
メソッド内で最初に取得するstate
値。0でない場合は、カレントオブジェクトのロックが他のスレッドに占有されていることを意味し、ロックを占有しているスレッドがカレントスレッドであるかどうかを判定する。 , 累積state
値が累積されます. これがリエントラントロックの具体的な目的です.実装, 累積state
値,state
ロック解放時のデクリメント値.
state
0 の場合は、CAS
操作を実行してstate
値を 1 に更新しようとします。更新が成功した場合は、現在のスレッドが正常にロックされたことを意味します。
スレッド 2 を例にとると、スレッド 1 がすでにstate
1 に変更しているため、スレッド 2 はCAS
変更された値では成功しませんstate
。ロックに失敗しました。
スレッド 2は実行後に false を返しtryAcquire()
、addWaiter(Node.EXCLUSIVE)
ロジックを実行してFIFO
待機キューに自身を追加します&#x