JavaキューシンクロナイザーフレームワークAQS実装の原則

序文

Javaでは、「ロック」は共有リソースへの複数のスレッドのアクセスを制御するために使用されます。Javaプログラミング言語を使用する友人は、ロック機能が同期キーワードによって実現できることを知っています。これにより、暗黙的にロックを取得できます。このキーワードを使用すると、ロックの取得と解放のプロセスを気にする必要がなくなりますが、利便性を提供しながら柔軟性が低下することも意味します。たとえば、最初にロックAを取得し、次にロックBを取得するシナリオがあります。ロックBを取得すると、ロックAが解放され、ロックCが取得されます。ロックCが取得された後、ロックBが解放され、ロックDが取得されます。類推により、このようなより複雑なシナリオで同期キーワードを実装することはより困難です。Java SE 5以降、syncキーワードと同じ機能を提供するために、Lockインターフェイスと一連の実装クラスが新たに追加されました。ロック取得などの割り込み応答同期機能を提供することに加えて、ロック取得と解放を表示する必要があります。操作とロックのタイムアウト取得。JDKで提供されるほとんどのLockインターフェース実装クラスは、シンクロナイザーAQSのサブクラスを集約して、マルチスレッドアクセス制御を実装します。ロックおよびその他の同期コンポーネントを構築するためのこの基本フレームワークであるキューシンクロナイザーAQS(AbstractQueuedSynchronizer)を見てみましょう。

1

AQSの基本的なデータ構造

1.1

同期キュー

キューシンクロナイザーAQS(以下シンクロナイザーと呼びます)は、主に内部FIFO(先入れ先出し)双方向キューに依存して同期状態を管理します。スレッドが同期状態を取得できない場合、シンクロナイザーは現在のスレッドや現在の待機状態などの情報は、内部で定義されたノードノードにカプセル化され、現在のスレッドをブロックしながらキューに追加されます。同期状態が解放されると、同期キューの最初のノードがウェイクアップされます。同期のステータスを再度取得しようとします。同期キューの基本構造は次のとおりです。

JavaキューシンクロナイザーフレームワークAQS実装の原則

1.2

キューノード

同期キューは、シンクロナイザーの静的内部クラスNodeを使用して、同期状態、スレッドの待機状態、先行ノード、および後続ノードを取得するスレッドの参照を保存します。

JavaキューシンクロナイザーフレームワークAQS実装の原則

同期キュー内のノードノードの属性名と特定の意味を次の表に示します。
JavaキューシンクロナイザーフレームワークAQS実装の原則
各ノードスレッドにはそれぞれ2つのロックモードがあります。SHAREDはスレッドが共有モードでロックを待機することを意味し、EXCLUSIVEはスレッドがスレッドを意味します。排他的な方法でロックを待ちます。同時に、各ノードのwaitStatusは、次の表に列挙されている値のみを取得できます:

JavaキューシンクロナイザーフレームワークAQS実装の原則

1.3

同期状態

シンクロナイザーは、stateという名前のint型変数を使用して、同期状態を表します。シンクロナイザーの主な用途は、継承です。サブクラスは、抽象メソッドを継承して実装することにより、同期状態を管理します。シンクロナイザーは、次の3つの変更方法を提供します。同期ステータス。

JavaキューシンクロナイザーフレームワークAQS実装の原則
排他ロックでは、同期状態の状態の値は通常0または1(再入可能ロックの場合、状態値は再入可能の数)であり、共有ロックでは、状態は保持されているロックの数です。

2

排他的な同期状態の取得と解放

シンクロナイザーは、排他同期状態を取得するためのacquire(int arg)メソッドを提供します。同期状態が取得されると、ロックが取得されます。このメソッドのソースコードは次のとおりです。


public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

メソッドは最初にtryAcquireメソッドを呼び出してロックの取得を試みます。メソッドのソースコードを見ると、シンクロナイザーがメソッドを実装していないことがわかります(UnsupportedOperationExceptionをスローするだけです)。このメソッドには、後続のメソッドの開発者が必要です。同期コンポーネント。これを実現するために、メソッドがtrueを返す場合は、現在のスレッドがロックを正常に取得したことを意味し、selfInterrupt()が呼び出されて現在のスレッドに割り込みます(PS:ここに全員への質問があります:なぜ中断するのですか?ロックを取得した後のスレッド?)、メソッドは終了して戻りますメソッドがfalseを返す場合は、現在のスレッドがロックの取得に失敗したことを意味します。つまり、他のスレッドが以前にロックを取得したことを意味します。この時点で、現在のスレッドと待機ステータス情報を同期キューに追加します。シンクロナイザーを見てみましょう。スレッドがロックを取得しない方法。ソースコードから、ロック取得に失敗すると、判定条件の後半と操作acquireQueued(addWaiter(Node.EXCLUSIVE)、arg)が実行されることがわかります。まず、ロックモードをNode.EXCLUSIVEとして指定し、 addWaiterメソッドを呼び出します。メソッドのソースコードは次のとおりです。


private Node addWaiter(Node mode) {
    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;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

ノードノードは、メソッドパラメータと現在のスレッドで指定されたロックモード(共有ロックまたは排他ロック)を介して構築されます。同期キューが初期化されている場合は、最初にテールからキューに参加しようとします。 compareAndSetTailメソッドは、アトミック性を確保して入力するために使用されます。メソッドのソースコードは、sun.miscパッケージで提供されるUnsafeクラスに基づいて実装されていることがわかります。同期キューへの最初の参加試行が失敗した場合、エンキュー操作のためにenqメソッドが再度呼び出され、enqメソッドのソースコードが次のようにフォローアップされます。

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

ソースコードから、同期キューの初期化の判断がメソッドに追加され、compareAndSetHeadメソッドがアトミック性を確保するために使用されることを除いて、キューに初めて参加しようとしたコードと類似していることがわかります。最下層もUnsafeクラスに基づいており、次に外側層が設定されます。for(;;)無限ループの場合、ループの唯一の終了条件は、の終わりからキューに入るだけです。キューが正常に返される、つまり、メソッドが正常に戻る場合は、キューが正常に入力されたことを意味します。この時点で、addWaiterが実行され、現在のノードノードに戻ります。次に、このノードをacquireQueuedメソッドの入力パラメーターとして使用して、他の手順を続行します。メソッドは次のとおりです。


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;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

このメソッドは基本的に無限ループ(スピン)を使用してロックを取得し、中断をサポートしていることがわかります。2つのフラグ変数がループ本体の外側で定義され、失敗したフラグがロックを正常に取得したかどうか、および中断されたフラグが待機中のプロセスが中断されました。このメソッドは、最初に先行ノードを介して現在ノードの先行ノードを取得します。現在ノードの先行ノードがヘッドノードである場合、tryAcquireが呼び出されてロックを取得しようとします。つまり、2番目のノードがロックを取得しようとします。なぜ2番目のノードから試してみたいのですか?ロックを取得するのはどうですか?その理由は、同期キューが本質的に二重リンクリストであるためです。二重リンクリストでは、最初のノードはデータを格納しません。これは仮想ノードですが、プレースホルダーとしてのみ機能します。実際にデータを格納するノードは、の2番目のノード。ロックが正常に取得された場合、つまりtryAcquireメソッドがtrueを返した後、ヘッドを現在のノードにポイントし、以前に見つかったヘッドノードpをキューから削除し、ロックマークが正常に取得されたかどうかを変更すると、endメソッドは割り込みマーク。現在のノードの先行ノードpがヘッドノードではないか、先行ノードpがヘッドノードであるが、ロック取得操作が失敗した場合、shouldParkAfterFailedAcquireメソッドが呼び出され、現在のノードノードをブロックする必要があるかどうかが判断されます。ここでの判断のブロックは、主に長期的な回転を防ぐためです。CPUは非常に大きな実行オーバーヘッドをもたらし、リソースを浪費します。メソッドのソースコードは次のとおりです。


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.
          */
        return true;
    if (ws > 0) {
        /*
          * Predecessor was cancelled. Skip over predecessors and
          * indicate retry.
          */
        do {
            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.
          */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

メソッドパラメータは、現在のノードの先行ノードと現在のノードです。先行ノードは、主にブロッキングが必要かどうかを判断するために使用されます。最初に、先行ノードの待機状態wsが取得されます。ノードステータスwsがSIGNALの場合、これは、先行ノードのスレッドの準備ができていることを意味します。、リソースが解放されるのを待つと、メソッドはtrueを返し、ブロックできることを示します。ws> 0の場合、ノードの状態は1つだけキャンセルされていることがわかります。 (値1)この条件が満たされた場合、ロックを取得するノードスレッドの要求がキャンセルされ、渡されます。do-whileループは、CANCELED状態のノードを楽しみにして、同期から削除します。それ以外の場合は、elseブランチに入り、compareAndSetWaitStatusアトミック操作を使用して、先行ノードの待機状態をSIGNALに変更します。上記の2つのケースを実行する必要はありません。blockingメソッドはfalseを返します。判定後にブロックする必要がある場合、つまりcompareAndSetWaitStatusメソッドがtrueを返す場合、現在のスレッドはparkAndCheckInterruptメソッドによってブロックおよび一時停止され、現在のスレッドの割り込みフラグが返されます。以下の方法:


private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

スレッドブロッキングはLockSupportツールクラスによって実装されます。ソースコードを深く掘り下げると、その下にあるレイヤーもUnsafeクラスに基づいていることがわかります。上記の2つのメソッドが両方ともtrueを返す場合、割り込みフラグが更新されます。ここでのもう1つの質問は、ノードの待機ステータスが、ロックを取得するためのCANCELEDノードスレッドの要求キャンセルステータスにいつ変更されるかということです。注意深い友人は、上記のacquireQueuedメソッドのソースコードのfinallyブロックが、失敗したタグに従ってcancelAcquireメソッドを呼び出すかどうかを決定することを発見した可能性があります。このメソッドは、ノードのステータスをCANCELLEDに変更するために使用されます。これはメソッドLeaveの特定の実装です。誰もが探索することができます。この時点で、AQS排他同期状態取得ロックプロセスが完了しました。フローチャートを使用してプロセス全体を見てみましょう。

JavaキューシンクロナイザーフレームワークAQS実装の原則

排他ロックの解放プロセスをもう一度見てみましょう。シンクロナイザーは解放メソッドを使用して排他ロックを解放します。メソッドのソースコードは次のとおりです。


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

最初にtryReleaseメソッドを呼び出して、ロック解除操作を実行しようとします。引き続きメソッドをフォローアップし、シンクロナイザーがUnsupportedOperationExceptionをスローすることを確認します。これは上記の排他的ロック取得のtryAcquireメソッドと同じです。開発者は次のことを行う必要があります。ロックを定義します。操作を解放します。
JavaキューシンクロナイザーフレームワークAQS実装の原則

JavaDocによると、falseが返された場合は、ロックの解放に失敗し、メソッドが終了したことを意味します。このメソッドがtrueを返す場合は、現在のスレッドがロックを正常に解放し、キュー内のロックの取得を待機しているスレッドに、ロック取得操作を実行するように通知する必要があることを意味します。最初にヘッドノードのヘッドを取得します。現在のヘッドノードがnullでなく、待機状態が初期状態(0)でない場合、スレッドブロッキングの一時停止状態はunparkSuccessorメソッドによって解放されます。このメソッドのソースコードは次のとおりです。次のとおりです。


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.
      */
    int ws = node.waitStatus;
    if (ws < 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.
      */
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

最初にヘッドノードの待機状態wsを取得します。状態値が負の場合(Node.SIGNALまたはNode.PROPAGATE)、CAS操作で初期状態(0)に変更してから、ヘッドの後続ノードを取得します。後続ノードがnullであるか、後続ノードのステータスがCANCELED(ロック取得要求がキャンセルされている)の場合、ノードがCANCELLED以外の場合は、キューの最後から開始して、ステータスがCANCELLED以外の最初のノードを見つけます。空ではない場合は、LockSupportのunparkメソッドを使用してウェイクアップします。メソッドの下部です。これは、Unsafeクラスのunparkによって実装されます。キューの最後からCANCELLED以外のステータスのノードを見つける必要がある理由は、排他ロックの取得に失敗した場合のエンキューのaddWaiterメソッドの以前の実装では、メソッドは次のとおりであるためです。

JavaキューシンクロナイザーフレームワークAQS実装の原則

上図の①でスレッドが実行され、②がまだ実行されていない場合、このとき、別のスレッドがunparkSuccessorメソッドを実行すると、ノードの隣に後続ポインタがあるため、前から後ろに検索できません。まだ割り当てられていないので、後ろから前に検索する必要があります。この時点で、排他的ロック解除操作は終了します。同様に、フローチャートを通じてロック解除プロセス全体を確認します。

JavaキューシンクロナイザーフレームワークAQS実装の原則

3

排他的な割り込み可能な同期ステータスの取得

シンクロナイザーは、割り込み応答取得ロック操作を実行するためのacquireInterruptiblyメソッドを提供します。メソッドのソースコードは次のとおりです。


public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}

このメソッドは、最初に現在のスレッドの中断ステータスをチェックします。中断されている場合は、中断された例外InterruptedExceptionを直接スローして、中断に応答します。それ以外の場合は、tryAcquireメソッドを呼び出して、ロックの取得を試みます。取得が成功した場合、メソッドは終了して戻ります。取得が失敗した場合は、doAcquireInterruptiblyメソッドを呼び出して、次のようにメソッドをフォローアップします。


private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

注意深く観察すると、このメソッドのソースコードは、エンキュー操作addWaiterがメソッドに組み込まれていることを除いて、上記のacquireQueuedメソッドのソースコードと基本的に同じであることがわかります。もう1つの違いは、判断時に直接スローされることです。ループ本体で中断されます。中断に応答する場合の例外として、2つの方法の比較は次のとおりです。

JavaキューシンクロナイザーフレームワークAQS実装の原則

その他の手順は、排他ロック取得と同じです。フローチャートは、割り込みに応答しないロック取得とほぼ同じですが、最初に割り込み例外をスローするスレッド中断ステータスチェックとループがもう1つある点が異なります。 。

4

同期ステータスを取得するための排他的タイムアウト

シンクロナイザーは、tryAcquireNanosメソッドを提供して、時間の経過に伴う同期状態(つまり、ロック)を取得します。このメソッドは、synchronizedキーワードではサポートされていなかったタイムアウト取得機能を提供します。このメソッドを使用すると、指定した時間内にロックを取得できます。 period nanosTimeout。取得された場合、ロックはtrueを返し、それ以外の場合はfalseを返します。メソッドのソースコードは次のとおりです。


public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}

最初に、tryAcquireメソッドが呼び出されてロックの取得が試行され、ロックの取得が成功するとすぐに戻ります。それ以外の場合は、doAcquireNanosメソッドが呼び出されてタイムアウトロック取得プロセスに入ります。以上のことから、シンクロナイザーのacquireInterruptiblyメソッドが同期状態の取得を待機しているときに、現在のスレッドが中断されると、中断された例外InterruptedExceptionがスローされ、すぐに戻ることがわかります。時間の経過とともにロックを取得するプロセスは、実際には、割り込みへの応答に基づいてタイムアウト取得の機能を追加しています。doAcquireNanosメソッドのソースコードは次のとおりです。


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);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L)
                return false;
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

上記のメソッドのソースコードから、タイムアウト取得の主な実現アイデアは、最初に現在の時刻とパラメータによって渡されたタイムアウト間隔の期限を使用してタイムアウトポイントを計算し、次にタイムアウトポイントを毎回使用することであることがわかります。ループが実行された時間現在の時間から期限が差し引かれ、残りの時間が得られます。nanosTimeout残り時間が0未満の場合、現在のロック取得操作がタイムアウトしたことを証明し、残りの時間がfalseの場合、メソッドはfalseを返します。 0より大きい。内部のスピンの実行は、上記のロック状態acquireQueuedメソッドの排他的同期取得と同じであることがわかります。つまり、現在のノードの先行ノードがヘッドノードである場合、tryAcquireが呼び出されて取得を試みます。ロックし、取得が成功すると戻ります。

JavaキューシンクロナイザーフレームワークAQS実装の原則

タイムアウト計算の違いに加えて、タイムアウトがロックの取得に失敗した後の動作にも違いがあり、現在のスレッドがロックの取得に失敗した場合、残りのタイムアウト時間nanosTimeoutが0未満であるかどうかが判断されます。 0未満の場合は、メソッドがすぐにタイムアウトしたことを意味します。それ以外の場合は、現在のスレッドをブロックおよび一時停止する必要があるかどうかを判断します。現在のスレッドをshouldParkAfterFailedAcquireメソッドで一時停止およびブロックする必要がある場合、残りのタイムアウト時間nanosTimeoutとspinForTimeoutThresholdをさらに比較する必要があります。spinForTimeoutThreshold値(1000ナノ秒)以下の場合、現在のスレッドはタイムアウトを待機せず、再びスピンアップします。後者の判断を追加する主な理由は、非常に短い時間(1000ナノ秒未満)での待機は正確ではないためです。この時点でタイムアウトが待機すると、nanosTimeout全体のタイムアウトを指定できるようになります。したがって、残りのタイムアウトが非常に短い場合、シンクロナイザーは再びスピンして時間の経過とともにロックを取得します。排他的タイムアウトを超えてロックを取得するプロセス全体は次のとおりです。

JavaキューシンクロナイザーフレームワークAQS実装の原則

5

共有同期ステータスの取得と解放

共有ロックは、その名前が示すように、複数のスレッドがロックを共有できることを意味します。シンクロナイザーでacquireSharedを使用して、共有ロック(同期状態)を取得します。メソッドのソースコードは次のとおりです。


public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

まず、tryAcquireSharedを使用して共有ロックを取得してみます。このメソッドは、シンクロナイザーでサポートされていない操作例外をスローするだけのテンプレートメソッドです。開発者は自分で実装する必要があります。同時に、メソッドの戻り値には3つの異なるものがあります。 3つの異なるタイプを表すタイプ。ステータス、その意味は次のとおりです。

0未満は、現在のスレッドがロックの取得に失敗したことを意味します

0に等しいということは、現在のスレッドがロックを正常に取得することを意味しますが、後続のスレッドはロックが解放されないとロックを取得できません。つまり、このロックは共有モードの最後のロックです。

0より大きい場合は、現在のスレッドがロックを正常に取得し、取得できるロックが残っていることを意味します。

メソッドtryAcquireSharedの戻り値が0未満の場合、つまりロックの取得が失敗した場合、メソッドdoAcquireSharedが実行され、メソッドはフォローアップを続行します。


private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

このメソッドは、最初にaddWaiterメソッドを呼び出して、現在のスレッドと、待機状態が共有モジュールであるノードをカプセル化し、待機同期キューに追加します。ノードのnextWaiter属性は、固定値Node.SHAREDであることがわかります。共有モード。次に、ループ内の現在のノードの先行ノードを取得します。先行ノードがヘッドノードの場合は、共有ロックの取得を試みます。戻り値が0以上の場合は、共有ロックが正常に取得されたことを意味します。 。setHeadAndPropagateメソッドが呼び出されてヘッドノードが更新され、使用可能なリソースがある場合は、伝播後、後続のノードがウェイクアップし、割り込みフラグがチェックされます。中断されている場合は、現在のスレッドに割り込み、メソッドは終了し、戻り値。戻り値が0未満の場合は、ロックの取得に失敗したことを意味します。共有ロックを取得するには、現在のスレッドを一時停止してブロックするか、スピンを続行する必要があります。setHeadAndPropagateメソッドの特定の実装を見てみましょう。

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);
    /*
     * Try to signal next queued node if:
     *   Propagation was indicated by caller,
     *     or was recorded (as h.waitStatus either before
     *     or after setHead) by a previous operation
     *     (note: this uses sign-check of waitStatus because
     *      PROPAGATE status may transition to SIGNAL.)
     * and
     *   The next node is waiting in shared mode,
     *     or we don't know, because it appears null
     *
     * The conservatism in both of these checks may cause
     * unnecessary wake-ups, but only when there are multiple
     * racing acquires/releases, so most need signals now or soon
     * anyway.
     */
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

まず、現在ロックを取得しているノードをヘッドノードとして設定します。次に、メソッドパラメーターpropagate> 0は、前のtryAcquireSharedメソッドの戻り値が0より大きいことを意味します。つまり、共有ロックがまだ残っていることを意味します。を取得できる場合は、現在のノードの後続ノードを取得します。後続ノードが共有ノードの場合は、ノードをウェイクアップしてロックの取得を試みます。doReleaseSharedメソッドは、シンクロナイザーの共有ロック解除のメインロジックです。

共有ロックの解放プロセスを見てみましょう。シンクロナイザーは、共有ロックを解放するためのreleaseSharedメソッドを提供します。メソッドのソースコードは次のとおりです。

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

最初にtryReleaseSharedメソッドを呼び出して、共有ロックの解放を試みます。メソッドはfalseを返し、ロックの解放が失敗したことを示し、メソッドはfalseを返します。それ以外の場合、ロックは正常に解放されます。次に、doReleaseSharedメソッドが実行されてウェイクアップします。後続のノードを確認し、逆方向に伝播できるかどうかを確認します。次のようにメソッドのフォローアップを続けます。


private void doReleaseShared() {
        /*
        * Ensure that a release propagates, even if there are other
        * in-progress acquires/releases.  This proceeds in the usual
        * way of trying to unparkSuccessor of head if it needs
        * signal. But if it does not, status is set to PROPAGATE to
        * ensure that upon release, propagation continues.
        * Additionally, we must loop in case a new node is added
        * while we are doing this. Also, unlike other uses of
        * unparkSuccessor, we need to know if CAS to reset status
        * fails, if so rechecking.
        */
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                        !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

排他ロック解除との違いは、共有モードでは状態の同期と解除を同時に実行でき、そのアトミック性はCASによって保証されていることがわかります。ヘッドノードが変更されてもサイクルは継続します。共有ノードが共有モードでウェイクアップするたびに、ヘッドノードがそれを指すため、共有ロックを取得できる後続のすべてのノードがウェイクアップできることが保証されます。

6

同期コンポーネントをカスタマイズする方法

JDKのシンクロナイザーに基づいて実装されたクラスのほとんどは、シンクロナイザーを継承する1つ以上のクラスを集約し、シンクロナイザーが提供するテンプレートメソッドを使用して内部同期状態の管理をカスタマイズし、この内部クラスを介して実装します。同期状態管理のは、実際にはある程度テンプレートモードを使用します。たとえば、JDKのリエントラントロックReentrantLock、読み取り/書き込みロックReentrantReadWriteLock、セマフォSemaphore、および同期ツールクラスCountDownLatch。ソースコードのスクリーンショットは次のとおりです。

JavaキューシンクロナイザーフレームワークAQS実装の原則

上記からわかるように、シンクロナイザーに基づいて、排他ロック同期コンポーネントと共有ロック同期コンポーネントを個別にカスタマイズできます。以下は、3つのスレッドのみが同時にアクセスできる同期ツールを実装することです。他のスレッドへのアクセスはブロックされます例としてTripletsLockを取り上げます。明らかに、このツールは共有ロックモードです。主なアイデアは、JDkにLockインターフェイスを実装して、lockメソッドを呼び出して取得するなどのユーザー指向のメソッドを提供することです。 TripletsLockクラス内には、シンクロナイザーAQSから継承されたカスタムシンクロナイザーSyncがあり、スレッドのアクセスと同期ステータスを制御するために使用されます。スレッドがlockメソッドを呼び出して取得する場合ロック、カスタムシンクロナイザーSyncは、ロックを取得した後、最初にロックを計算します。状態を同期し、次に安全でない操作を使用して、同期状態の更新のアトミック性を確認します。同時にアクセスできるスレッドは3つだけなので、同期状態状態の初期値は3で、現在使用可能な同期リソースの数を表します。スレッドがロックを正常に取得すると、同期状態状態は1減少し、スレッドがロックを正常に解放すると、同期状態は次のようになります。同期状態の値の範囲は0、1、2、および3です。同期状態が0の場合、同期が利用できないことを意味します。リソース、この時点でスレッドアクセスがある場合、ブロックされます。 。このカスタム同期コンポーネントの実装コードを見てみましょう。


/**
 * @author mghio
 * @date: 2020-06-13
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class TripletsLock implements Lock {

  private final Sync sync = new Sync(3);

  private static final class Sync extends AbstractQueuedSynchronizer {
    public Sync(int state) {
      setState(state);
    }

    Condition newCondition() {
      return new ConditionObject();
    }

    @Override
    protected int tryAcquireShared(int reduceCount) {
      for (; ;) {
        int currentState = getState();
        int newState = currentState - reduceCount;
        if (newState < 0 || compareAndSetState(currentState, newState)) {
          return newState;
        }
      }
    }

    @Override
    protected boolean tryReleaseShared(int count) {
      for (; ;) {
        int currentState = getState();
        int newState = currentState + count;
        if (compareAndSetState(currentState, newState)) {
          return true;
        }
      }
    }
  }

  @Override
  public void lock() {
    sync.acquireShared(1);
  }

  @Override
  public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
  }

  @Override
  public boolean tryLock() {
    return sync.tryAcquireShared(1) > 0;
  }

  @Override
  public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
  }

  @Override
  public void unlock() {
    sync.releaseShared(1);
  }

  @Override
  public Condition newCondition() {
    return sync.newCondition();
  }
}

20のスレッドテストを開始して、カスタム同期ツールクラスTripletsLockが期待を満たしているかどうかを確認しましょう。テストコードは次のとおりです。


/**
 * @author mghio
 * @date: 2020-06-13
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class TripletsLockTest {
  private final Lock lock = new TripletsLock();
  private final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

  @Test
  public void testTripletsLock() {
    // 启动 20 个线程
    for (int i = 0; i < 20; i++) {
      Thread worker = new Runner();
      worker.setDaemon(true);
      worker.start();
    }

    for (int i = 0; i < 20; i++) {
      second(2);
      System.out.println();
    }
  }

  private class Runner extends Thread {
    @Override
    public void run() {
      for (; ;) {
        lock.lock();
        try {
          second(1);
          System.out.println(dateFormat.format(new Date()) + " ----> " + Thread.currentThread().getName());
          second(1);
        } finally {
          lock.unlock();
        }
      }
    }
  }

  private static void second(long seconds) {
    try {
      TimeUnit.SECONDS.sleep(seconds);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

テスト結果は次のとおりです。

JavaキューシンクロナイザーフレームワークAQS実装の原則

上記のテスト結果から、同時にロックを取得できるのは3つのスレッドのみであることがわかります。これは予想どおりです。ここで明確にする必要があるのは、ロック取得プロセスが不公平であるということです。

7

総括する

この記事では、主にシンクロナイザーでの基本的なデータ構造、排他的および共有の同期状態の取得と解放のプロセスを分析します。レベルが限られているため、議論のためにメッセージを残してください。キューシンクロナイザーAbstractQueuedSynchronizerは、JDKで多くのマルチスレッド同時実行ツールを実現するための基本的なフレームワークです。詳細な調査と理解は、その機能と関連ツールをより適切に使用するのに役立ちます。(追記:投稿されたコードはたくさんあるので、公式アカウントで読んだり表示したりするのは簡単ではないかもしれません。個人ブログに移動できます:https//www.mghio.cn

おすすめ

転載: blog.51cto.com/15075507/2607593