blog

ReentrantLockのソースコード、一緒に図を描いて見よう!

再入可能なミューテックスロックは、基本的な動作とセマンティクスは暗黙的なモニターロックと同じですが、はるかに強力です。 相互に排他的:同時にロックを取得できるのは1つのスレッドのみで、他のスレッドがロ...

May 21, 2020 · 9 min. read
シェア

序文



JUCパッケージの下にAQSのソースコードを読んだ後、多くの疑問があり、最大の問題は、状態の意味は何ですか?そして、AQSは主にキューインとアウトを定義しますが、リソースを取得し、リソースを解放するサブクラスに達成するために与えられている、そのサブクラスはどのように達成するためですか?以下は、ReentrantLockの理解の始まりです。



はじめに

再入可能なミューテックスロックは、暗黙的なモニターロックの同期と基本的な動作とセマンティクスは同じですが、より強力です。

には次のような特徴があります:

  1. 相互排他性:同時にロックを取得できるのは1つのスレッドのみで、ロックを取得しようとする他のスレッドはブロックされ、ロック内に保持されるAQSブロッキングキューに入れられます。
  2. 再エントリー:状態変数を維持し、初期値は0、スレッドがロックを取得すると、CASを使用して状態が1に更新され、このスレッドが再度ロックを取得するよう要求すると、CASを使用して状態がインクリメントされ、取得を繰り返す回数(状態)は最大2147483647回です。この制限を超えようとすると、ロック・メソッドからエラーがスローされます。
  3. 公正/非公正: 初期化時に、コンストラクタにパラメータを渡すことで、ロックを公正ロックにするか非公正ロックにするかを指定できます。true に設定すると公平なロックとなり、 ロックを争うスレッドは最も長く待ったスレッドを優先します。

基本的な使い方

class X {
 private final ReentrantLock lock = new ReentrantLock();
 // ...
 public void m() {
 lock.lock(); // block until condition holds
 try {
 // ... method body
 } finally {
 lock.unlock()
 }
}
}

質問は?

まずはこの記事を読みながら、AQSについてよく理解してください。グラフィカルな説明 AQS

  1. ReentrantLock AQSとの関係は?
  2. スレッドはどのようにしてロックを獲得するのですか?
  3. ロックの再連続性はどのようにして実現するのですか?
  4. ロックの獲得に失敗し、ブロックされた現在のスレッドのその後の動作は?
  5. フェア・ロックとそうでないロックの表現は?
  6. ロックはどのように解除されるのですか?

ソースコードを読んだり、上記の質問に沿って図を描いたりして分析します。

ソースコード解析

基本構造

ReentrantLockクラスはインターフェイスLockを実装し、インターフェイスLockでロックを使用する際のメソッドを定義しています:

public interface Lock {
 
 // ロックが獲得できなければブロックされる。
 void lock();
 // ロックを獲得する、獲得できなければブロックする。割り込みに対応する。
 void lockInterruptibly() throws InterruptedException;
 // ロックの獲得を試み、獲得できればtrueを返し、獲得できなければreturnを返す。 false
 boolean tryLock();
 // ノードがロックを獲得できなかった場合、指定された時間待機し、割り込みに応答する。
 boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
 //  
 void unlock();
}

ReentrantLockはLockインターフェイスを実装し、これらのメソッドを実装しているだけですが、ReentrantLockとAQSの関係は?それは内部でどのように実装されているかによります。

public class ReentrantLock implements Lock, java.io.Serializable {
 private final Sync sync;
 // ロックの同期制御の基本クラス。 サブクラスはフェア・バージョンと非フェア・バージョンに固有である。 保持しているロックの数を示すためにAQS状態を使用する。
 abstract static class Sync extends AbstractQueuedSynchronizer { 
 //   ...
 }
 static final class NonfairSync extends Sync { 
 // 非フェア・ロック・ロジックは省略 ...
 }
 static final class FairSync extends Sync { 
 // フェア・ロック・ロジック省略 ...
 }
 // デフォルトの非フェア・ロック
 public ReentrantLock() {
 sync = new NonfairSync();
 }
 // 渡されたパラメータによって、ロックがフェアか非フェアかを指定することができる。
 public ReentrantLock(boolean fair) {
 sync = fair ? new FairSync() : new NonfairSync();
 }
}

上のコードでわかるように

  1. 基本的なロックの制御はNonfairSyncとFairSyncが行っており、その親クラスであるSyncはAQSを継承しているため、ReentrantLockの実装はAQSに関連しています。
  2. NonfairSync は非フェアロック実装ロジックを表し、FairSync はフェアロック実装ロジックを表します。
  3. コンストラクタはパラメータを渡しますが、 このパラメータは初期化されるとデフォルトで NonfairSync の非フェアロックになります。また、ロックの公正/非公正を指定することも可能で、 その場合は公正ロックを true、非公正ロックを false とします。

ReentrantLockとAQSの関係は、ロックプロセスを通して分析されています。

ロック

ご覧のように、デフォルトでは非フェアロックが宣言され、lockメソッドの内部でsync.lock()が呼び出されています。

final void lock() {
 // CASによる状態値の設定 0 -> 1
 if (compareAndSetState(0, 1))
 // セットアップに成功すると、現在のスレッドがロックを獲得する。
 setExclusiveOwnerThread(Thread.currentThread());
 else
 // セットアップに失敗した場合、AQSメソッドが呼び出され、ロックの獲得を試みる。
 acquire(1);
}
  1. 最初にCASを使ってstateの値を更新すると、stateがロックの状態を表していることがわかります。 0 アンロック、1 ロック。
  2. セットアップに失敗すると、AQS acquire(1);メソッドが呼び出されます。

AQSの取得コードをご覧ください:

public final void acquire(int arg) {
 // tryAcquire 状態の取得を試み、失敗した場合はキューに追加される。
 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
 selfInterrupt();
}

AQSのソースコードを分析すると、状態の値を取得する試みとしてtryAcquireを導入済みですが、これはAQSでは利用できず、サブクラスで実装されています。そのため、このコードの一部は、独自のビジネスロジックを実装するために、NonfairSync クラスに残っています。

static final class NonfairSync extends Sync {
 // NonfairSync  
 protected final boolean tryAcquire(int acquires) {
 // 親クラスのメソッドを呼び出す
 return nonfairTryAcquire(acquires);
 }
}
abstract static class Sync extends AbstractQueuedSynchronizer {
 // NonfairSync AQSの親クラスであるSyncに実装がある。
 // state   1
 final boolean nonfairTryAcquire(int acquires) {
 // 現在のスレッドを取得する
 final Thread current = Thread.currentThread();
 //   state
 int c = getState();
 // cが 0 
 if (c == 0) {
 // casを使って 1
 if (compareAndSetState(0, acquires)) {
 // 保持スレッドを現在の
 setExclusiveOwnerThread(current);
 return true;
 }
 } else if (current == getExclusiveOwnerThread()) {
 // 現在のスレッドが
 // 状態を蓄積する
 int nextc = c + acquires;
 // intの最大値を超えないこと。  + 1 = -
 if (nextc < 0) // overflow
 throw new Error("Maximum lock count exceeded");
 // ステート値を設定する
 setState(nextc);
 return true;
 }
 return false;
 }
}
  1. 現在のスレッドがロックされている場合、CASを使用して状態を0から1に更新します。更新が成功するとロックが取得され、更新が失敗すると取得に失敗します。
  2. 更新に失敗した場合、AQS acquire(1);メソッドが呼び出され、1が渡されます。
  3. tryAcquireはロックの再取得を試みます。
    1. stateが0の場合は取得を試みます。stateが0の場合、取得を試みます;
    2. stateが0でない場合は、現在のスレッドが保持しているかどうかを判断し、保持している場合はstateを追加します。
  4. tryAcquireがロックの獲得に失敗すると、AQSのacquireQueuedロジックに従ってノードを作成し、待ち行列に追加します。

フロー図を以下に示します:

  • 最初は単一のスレッド

  • この時点で、他のスレッドが入ってきてロック獲得を要求します。

  • ロックフローチャート

フェア・ロックはどのように現れるか

static final class FairSync extends Sync {
 private static final long serialVersionUID = -L;
 final void lock() {
 acquire(1);
 }
 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;
 }
}

コードを取り出して比較してください:

フェアロックの判定条件が追加されていることがわかります。

!hasQueuedPredecessors()

AQSのhasQueuedPredecessorsメソッドは、現在のスレッドの前にキューに入っているスレッドがあればtrueを返し、現在のスレッドがキューの先頭にあるか、キューが空であればfalseを返します。

コードは以下の通り:

public final boolean hasQueuedPredecessors() {
 Node t = tail; 
 Node h = head;
 Node s;
 return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}

現在のロックの時点でノードがすでにキューに入っている場合は、ノードの最後尾に移動してキューに入れます。

この時点で、あなたは基本的にフェアなロックとそうでないロックの違いを知っています:

**非フェアロック:**は、キューにノードがあるかどうかに関わらずロックの取得を試み、取得に失敗した場合はacquireメソッドに入り、やはりキューに入る前に一度取得を試みます。

**公平なロック:**すでにキューにノードがあるので、ノードの後ろに行き、自分自身をキューに入れます。

tryLock


public boolean tryLock() {
 return sync.nonfairTryAcquire(1);
}

SyncのnonfairTryAcquireを直接呼び出し、ロックの取得を試み、取得に失敗してfalseを返し、ロックを取得するか、現在のスレッドがロックを保持し、状態を蓄積してtrueを返します。

unlock

public void unlock() {
 sync.release(1);
}

Unlockは、リソースを解放するためにAQSのreleaseメソッドを直接呼び出していることがわかります。

public final boolean release(int arg) {
 if (tryRelease(arg)) {
 Node h = head;
 if (h != null && h.waitStatus != 0)
 unparkSuccessor(h);
 return true;
 }
 return false;
}

この部分については、AQSにも記述されており、tryReleaseはサブクラスによって実装されることが述べられていますが、ここではReentrantLockにおけるtryReleaseの実装に焦点を当てます。

// リソースを解放するには 1
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. 1操作の現在の状態を取得します;
  2. 現在のスレッドが保持スレッドかどうかを判定します;
  3. リリース後にstateが0の場合、保持スレッドをnullに設定します;
  4. state の値を更新して返します。

まとめ

上記のソースコードと図面があれば、あなたは基本的に、あなたが最初に抱いた疑問に対する答えを得ることができます:

A: ReentrantLockのstateはロック状態を表し、0はロックを取得していないスレッド、1以上はロックを取得しているスレッド、1以上はロックを取得しているスレッドが何度も再入力していることを意味します。

Q: ReentrantLockとAQSの関係は?

A: ReentrantLockの内部はAQSの実装に基づいており、ロックの状態や待ちキューへの入り方、ロックの解放などはAQSの実装に基づいています。 ReentrantLockのフェアロックと非フェアロックはNonfairSync、FairSyncで実装されており、その親クラスであるSyncはAQSを継承しています。

Q: スレッドはどのようにしてロックを獲得するのですか?

A: スレッドは状態フィールドの状態を変更することでロックを取得します。

Q: ロックの再連続性はどのようにして実現するのですか?

A: 現在のスレッドが状態を0でないと判断した場合、それはロックが獲得されたことを意味します。

Q: 現在のスレッドがロック獲得に失敗したときにブロックされる後続アクションは何ですか?

A: 取得に失敗すると、AQSの待ち行列に入れられ、待ち行列の中でループして前のノードが先頭かどうかを監視し、先頭であれば再度ロックの取得を試みます。

Q: フェア・ロックとノンフェア・ロックはどのように表現されますか?

A: 公正なロックとは、現在のキューにすでにキューイングされているスレッドがある場合、そのスレッドが自分自身の真後ろに位置することに主に反映されます。非フェアロックとは、現在のキューにキューイングされているスレッドがないことに関係なく、ロックを獲得するために状態を変更しようとするものです。

Q: ロックの解除方法は?

A: ロックによってリソースが解放される、つまり状態が-1になります。状態が-1の後に0になると、そのノードは解放され、後続のノードがロックの取得を試みます。AQS関連ロジックはこちら。

Read next

NANDフラッシュの基本

NANDフラッシュは不揮発性記憶媒体の一種で、一般的なUSBフラッシュドライブ、TFカード/SDカード、SSDのほとんどがNANDフラッシュで構成されています。 この記事は主にその構成と動作原理を紹介します。 フラッシュの基本単位はフローティングゲートトランジスタで、その状態は...

May 21, 2020 · 8 min read