序文
最近、Java の LinkedBlockingQueue データ構造を調べていると、その中に ReentrantLock が使用されていることがわかりました。LinkedBlockingQueue のスレッド セーフ原理をよりよく理解するには、ReentrantLock の背後にある原理を理解する必要があります。この記事では、ロック、ロック解除について紹介します。 ReentrantLock の公平性とロックの舞台裏、不公平なロックの詳細。
1、リエントラントロック
1.1、ReentrantLock データ構造
ReentrantLock には Sync オブジェクトの同期があり、Sync は AbstractQueuedSynchronizer (AQS) から継承されます。Sync の具体的な実装には 2 つの NonfairSync (不公平なロック) と FairSync (公平なロック) があり、継承関係は次のとおりです。
どちらも AQS から継承されているため、NofairSync と FairSync の 2 つのロックのデータ構造は同じであり、主なクラス メンバーは次のとおりです。
-- head、tail:ロックの取得を待機しているリンク リスト キューを保存します。head はリンク リストの先頭を指し、tail はリンク リストの末尾を指します。
-- state: state。0 より大きい場合、ロックが使用されていることを意味します。
-- exclusiveOwnerThread:ロックを占有しているスレッドを保存します。
1.2 ReentrantLockの初期化
ReentrantLock には 2 つのコンストラクタがあります。デフォルトのコンストラクタは不公平なロックを使用します。設定パラメータが true の場合は公平なロックです。コードは次のとおりです。
public ReentrantLock() {
sync = new NonfairSync(); //作者注:默认非公平锁
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync(); //作者注:通过fair决定使用公平锁还是非公平锁
}
2. フェアロック
ReentrantLock の lock メソッドが呼び出されたとき、実際には、最終的に Sync の lock メソッドが呼び出されます。ロックがフェア ロックの場合は、FairSync の lock メソッドが呼び出されます。ロックを取得する全体のコード呼び出しプロセスは次のとおりです。
//作者注:FairSync的lock方法
final void lock() {
acquire(1);
}
//作者注:AQS的acquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//作者注:FairSync的tryAcquire
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
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;
}
1. tryAcquire メソッドを呼び出して、ロックの取得を試みます。
-- 現在のロックがスレッドによって使用されておらず (state=0)、キュー内に待機中のスレッドがない場合 (!hasQueuedPredecessors())、ロックを取得し、状態を 1 に設定し、exclusiveOwnerThread を現在のスレッドに設定して、true を返します。買収が成功した場合。
-- 現在のスレッドによってロックが取得されている場合は、state=state+1 を設定し、ロックが正常に取得された場合は true を返します。(これはリエントラント ロックと呼ばれます。つまり、同じスレッド内でロックを複数回取得できます。もちろん、スレッドはロックを数回取得し、最終的にロックを数回解放します)。
-- ロックの取得に失敗しました。false を返します。
2. ロックの取得が失敗した場合は、addWaiter(Node.EXCLUSIVE), arg) メソッドを呼び出し、現在のスレッドを待機キューに入れ、他のスレッドがロックを解放するのを待ちます。スレッド 0 が未解放状態でロックを取得すると、ロックを取得したい後続のスレッドがリンク リストに配置されます。具体的なデータ構造は次のとおりです。
(この図はおおよその状況であり、それに関連する一部の詳細は示されていません。たとえば、ヘッドが指しているのは実際には空のノードであり、実際のスレッド ノードの後には空のノードが続きます)
3. 不当なロック
不公平なロックと公正なロックのロック プロセスの唯一の違いは次のとおりです。
//作者注:NonFairSync的lock方法
final void lock() {
if (compareAndSetState(0, 1)) //作者注:直接尝试获取锁,有可能插队成功。
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); //作者注:如果获取失败,也要乖乖的去队列排队去。
}
2 からわかるように、フェア ロックはロックを取得する前に、まずロックを占有している他のスレッドがあるかどうか (状態が 0 に等しいかどうか)、およびキュー内に待機中のスレッドがあるかどうかを判断します。待機キューに追加されます。
ただし、不当ロックは待機キューに待機スレッドがあるかどうかを判断せず、直接ロックの状態をリセット(compareAndSetState)しようとし、設定が成功した場合はロックが解放されたことを意味し、スレッドを直接ロックします。ロックを占有します。したがって、私の理解は次のとおりです。
公平なロック:すべてのスレッドは、キュー内に待機中のスレッドがあるかどうかを最初に判断する必要があり、存在する場合は、素直にキューに入れなければなりません。
不公平なロック:キュー内に待機中のスレッドがあるかどうかを判断せず、直接ロックを取得しようとします (つまり、キューにジャンプします)。これにより、キュー内のスレッドよりも先にロックを取得する可能性があります。
もちろん、現在のスレッドがロックを取得しなかった場合、最終的には素直にキューに移動します (その後の Acquire(1) の実行プロセスはフェア ロックとまったく同じです)。
4. ロックの解除
ロック解除エントリ機能は以下のとおりです。
//作者注:AbstractQueuedSynchronizer里的方法
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
//作者注:Sync里的方法
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
1. tryRealease(arg) で、state を 0 に設定し、exclusiveOwnerThread を null に設定します。
2. 特定のキュー内のスレッドがロックを再取得するために呼び出している場所が見つかりません。(unparkSuccessor メソッドではありません)
5、状態
Condition は Java 1.5 でのみ登場しました 従来の Object の wait() と Notice() を置き換えてスレッド間の連携を実現するために使用されます Object の wait() と Notice() を使用するのと比較して、Condition の await() 、 signal() を使用しますこれは、スレッド間のコラボレーションを実現するためのより安全かつ効率的な方法です。したがって、一般的には Condition を使用することが推奨されており、ブロッキング キューは実際に Condition を使用してスレッド間のコラボレーションをシミュレートします。
実際の使用では、Condition と Sync を併用しますが、Condition と Sync の関係を理解するために、次のように 2 つの Condition を設定し、実行後にデバッグして 2 つの Condition に何が含まれているかを確認します。
ReentrantLock lock = new ReentrantLock();
Condition cond1 = lock.newCondition();
Condition cond2 = lock.newCondition();
デバッグ オブジェクト内の情報を確認します。
cond1 と cond2 が同期を共有していることがわかります。したがって、上記の条件を確立する方法により、生成されるデータ構造は次のとおりであることがわかります。
待機中のスレッドとブロックされたスレッドの違いは次のとおりです。
待機中のスレッド:ロックを取得する権利があります。
ブロックされたスレッド:ロックを取得する権利がないため、ロックを取得する権利を取得するには、起動されて AQS 待機キューに参加する必要があります。
5.1、待機とシグナル: 手動でスレッドをブロックし、スレッドを呼び出す
条件の await メソッドを呼び出して、現在のスレッドをブロックする必要があること、現在のスレッドによって保持されているロックを解放する必要があること、および現在のスレッドを条件のブロック キューに追加する必要があることを示します。呼び出しコードは次のとおりです。
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//作者注:将当前线程添加到Condition的阻塞线程队里的末尾
Node node = addConditionWaiter();
//作者注:释放当前线程持有的锁
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
//作者注:将当前线程挂起
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//作者注:如果当前线程被唤起,尝试去获取锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
実行プロセスをより直観的に確認するために、デバッグ テストに単純なコードを使用し、次のような単純なコードを作成します。
public class ReentrantLockTest {
public static final ReentrantLock lock = new ReentrantLock();
public static final Condition condition = lock.newCondition();
public static class Td1 extends Thread {
ReentrantLock lock ;
Condition condition ;
public Td1(ReentrantLock lock,Condition condition){
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
try {
lock.lock(); // 代码1
condition.await(); //代码2
System.out.println("thread-0");
lock.unlock(); //代码3
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static class Td2 extends Thread {
ReentrantLock lock ;
Condition condition ;
public Td2(ReentrantLock lock,Condition condition){
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
try {
Thread.sleep(5000); //为了保证先运行线程1,后运行线程2,在这里暂停5秒
lock.lock(); //代码4
condition.signal(); //代码5
System.out.println("thread-1");
lock.unlock(); //代码6
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Td1 td1 = new Td1(lock,condition);
Td2 td2 = new Td2(lock,condition);
td1.start();
td2.start();
}
}
1.コード 1を実行します。次のようにロックのデバッグ データが表示されます。ロックは Thread-0 (exclusiveOwnerThread=thread-0、state=1) によって占有されています。
2.コード 2まで実行します。この時点ではスレッドが一時停止されているため、特定のデータを確認するために、await コード内の int中断モード = 0 の行までデバッグします。デバッグ データは次のように確認できます。ロックが解放されている間、キュー内の条件に -0 が追加されます (exclusiveOwnerThread=null、state=0)。
3.コード 4まで実行します。この時点では、スレッド 0 はまだ条件キュー内にあり、ロックはスレッド 1 によって占有されています。
4.コード 5まで実行します: 条件をウェイクアップし、スレッド 0 を条件キューからクリアして、ロック待機キューに追加します。ただし、この時点ではスレッド 1 がロックを解放していないため、ロックはまだ保持されています。スレッド-1。
5.コード 6まで実行します。この時点で、スレッド 1 はロックを解放します。これは、実行速度が速く、ロックのアイドル期間が確認されずにスレッド 0 によってキャプチャされたためです。
6. この時点で、スレッド 0 が起動され、コードはコード 3まで実行され、スレッド 0 がロックを解放し、コード全体が実行されて終了します。
以上が ReentrantLock と Condition を併用する全体のプロセスですが、デバッグプロセスからは、内部データ構造全体とロック保持の変化がはっきりと確認できます。
6. もう一度説明します
この記事では、全体的なロック保持の変更と、ReentrantLock と Condition の内部データ構造のフレームワークの説明を提供します。実際、内部ロックの実装メカニズムには多くの詳細がありますが、ここには示されていません。ロックがどのように取得および解放されるかを明確に確認してください。具体的な仕組みを知りたい場合は、以下の著者の文章を参照してください。非常に詳しいですが、センスが必要で、考えても理解できるものではありません。