ホームページ  >  記事  >  Java  >  Java シンクロナイザー AQS アーキテクチャはどのようにしてロックを解放し、キューを同期しますか

Java シンクロナイザー AQS アーキテクチャはどのようにしてロックを解放し、キューを同期しますか

WBOY
WBOY転載
2023-05-11 17:16:151516ブラウズ

    はじめに

    AQS は内容が多すぎるため、前半を読んでいない学生も振り返ることができるように 2 つの章に分けました。この章の前半では、ロックの基本的な概念、基本的なプロパティ、ロックの取得方法などについて説明します。この章では、主にロックと同期キューを解放する方法について説明します。

    1. ロックを解放します

    ロックの解放のトリガー時間は、一般的に使用される Lock.unLock () メソッドであり、その目的は、スレッドがリソースへのアクセスを解放できるようにすることです (紫色の部分を参照)プロセスの全体的なアーキテクチャ図のルート)。

    ロックの解除も排他ロックの解除と共有ロックの解除の2つに分かれますので、分けて見てみましょう。

    1.1. 排他ロックの解放 解放

    排他ロックの解放は比較的簡単です. キューの先頭から開始して次のノードを見つけます. 次のノードが空の場合は,末尾から開始してステータスが解除されていないノードを見つけてノードを解放します ソースコードは次のとおりです:

    // unlock 的基础方法
    public final boolean release(int arg) {
        // tryRelease 交给实现类去实现,一般就是用当前同步器状态减去 arg,如果返回 true 说明成功释放锁。
        if (tryRelease(arg)) {
            Node h = head;
            // 头节点不为空,并且非初始化状态
            if (h != null && h.waitStatus != 0)
                // 从头开始唤醒等待锁的节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    // 很有意思的方法,当线程释放锁成功后,从 node 开始唤醒同步队列中的节点
    // 通过唤醒机制,保证线程不会一直在同步队列中阻塞等待
    private void unparkSuccessor(Node node) {
        // node 节点是当前释放锁的节点,也是同步队列的头节点
        int ws = node.waitStatus;
        // 如果节点已经被取消了,把节点的状态置为初始化
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        // 拿出 node 节点的后面一个节点
        Node s = node.next;
        // s 为空,表示 node 的后一个节点为空
        // s.waitStatus 大于0,代表 s 节点已经被取消了
        // 遇到以上这两种情况,就从队尾开始,向前遍历,找到第一个 waitStatus 字段不是被取消的
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 这里从尾迭代,而不是从头开始迭代是有原因的。
            // 主要是因为节点被阻塞的时候,是在 acquireQueued 方法里面被阻塞的,唤醒时也一定会在 acquireQueued 方法里面被唤醒,唤醒之后的条件是,判断当前节点的前置节点是否是头节点,这里是判断当前节点的前置节点,所以这里必须使用从尾到头的迭代顺序才行,目的就是为了过滤掉无效的前置节点,不然节点被唤醒时,发现其前置节点还是无效节点,就又会陷入阻塞。
            for (Node t = tail; t != null && t != node; t = t.prev)
                // t.waitStatus <= 0 说明 t 没有被取消,肯定还在等待被唤醒
                if (t.waitStatus <= 0)
                    s = t;
        }
        // 唤醒以上代码找到的线程
        if (s != null)
            LockSupport.unpark(s.thread);
    }

    1.2. 共有ロックの解放 releaseShared

    The共有ロックを解放するメソッドは releaseShared で、これは主に 2 つのステップに分かれています:

    tryReleaseShared は現在の共有ロックを解放しようとします。失敗した場合は false を返します。成功した場合は 2 に進みます。

    現在のノードの後続のブロッキング ノードをウェイクアップします。このメソッドは前にも見たことがあります。スレッドは共有を取得します。ロックすると、その背後にあるノードをウェイクアップします。メソッド名は doReleaseShared です。

    releaseShared のソース コードを見てみましょう:

    // 共享模式下,释放当前线程的共享锁
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            // 这个方法就是线程在获得锁时,唤醒后续节点时调用的方法
            doReleaseShared();
            return true;
        }
        return false;
    }

    2. 条件付きキューの重要なメソッド

    条件付きキューのメソッドを見る前に、まず理解する必要があります。同期キューがある場合でも、条件付きキューが必要ですか?

    主な理由は、すべてのシナリオが同期キューで処理できるわけではないためです。ロック キューが結合されるシナリオが発生した場合は、ロック条件が必要です。まず、Lock を使用して、どのスレッドがロックを取得できるかを判断します。どのスレッドがロックを取得する必要があるか。同期キューに入れられてブロックされます。ロックを取得した複数のスレッドがキューがいっぱいまたは空になった場合、Condition を使用してこれらのスレッドを管理し、これらのスレッドをブロックして待機させ、その後、適切なタイミングで通常どおりウェイクアップできます。時間。 。

    同期キューは条件付きキューと組み合わせて使用​​され、ロック キューのシナリオで最もよく使用されます。

    つまり、条件付きキューも不可欠な部分です。

    次に、条件キューのより重要なメソッドをいくつか見てみましょう。次のメソッドはすべて、ConditionObject 内部クラスにあります。

    2.1. キューに入って await を待ちます

    // 线程入条件队列
    public final void await() throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        // 加入到条件队列的队尾
        Node node = addConditionWaiter();
        // 标记位置 A
        // 加入条件队列后,会释放 lock 时申请的资源,唤醒同步队列队列头的节点
        // 自己马上就要阻塞了,必须马上释放之前 lock 的资源,不然自己不被唤醒的话,别的线程永远得不到该共享资源了
        int savedState = fullyRelease(node);
        int interruptMode = 0;
        // 确认node不在同步队列上,再阻塞,如果 node 在同步队列上,是不能够上锁的
        // 目前想到的只有两种可能:
        // 1:node 刚被加入到条件队列中,立马就被其他线程 signal 转移到同步队列中去了
        // 2:线程之前在条件队列中沉睡,被唤醒后加入到同步队列中去
        while (!isOnSyncQueue(node)) {
            // this = AbstractQueuedSynchronizer$ConditionObject
            // 阻塞在条件队列上
            LockSupport.park(this);
            if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                break;
        }
        // 标记位置 B
        // 其他线程通过 signal 已经把 node 从条件队列中转移到同步队列中的数据结构中去了
        // 所以这里节点苏醒了,直接尝试 acquireQueued
        if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
            interruptMode = REINTERRUPT;
        if (node.nextWaiter != null) // clean up if cancelled
            // 如果状态不是CONDITION,就会自动删除
            unlinkCancelledWaiters();
        if (interruptMode != 0)
            reportInterruptAfterWait(interruptMode);
    }

    await メソッドには特別な注意が必要な点がいくつかあります:

    上記のコードは位置 A をマークします。ノードの準備が整う前条件付きキューに入るには、現在保持されているロックが最初に解放されなければなりません。そうでない場合は、条件キューに入り、他のスレッドはロックを取得できなくなります。上記のコードは、位置 B をマークします。この時点で、ノードはCondition.signal または signalAll メソッドによって起動されます。この時点で、ノードは同期キューに正常に転送されているため (アーキテクチャ全体図の青いプロセス)、acquireQueued メソッドを直接実行できます。ノードはソースコードは名前に Waiter を使用することを好むため、条件キューで Waiter が実際には Node であることがわかります。

    await メソッドには、addConditionWaiter と unlinkCancelledWaiters という 2 つの重要なメソッドがあります。1 つずつ見てみましょう。

    2.1.1, addConditionWaiter

    addConditionWaiter メソッドは主にノードを条件キューに入れます。メソッドのソース コードは次のとおりです:

    // 增加新的 waiter 到队列中,返回新添加的 waiter
    // 如果尾节点状态不是 CONDITION 状态,删除条件队列中所有状态不是 CONDITION 的节点
    // 如果队列为空,新增节点作为队列头节点,否则追加到尾节点上
    private Node addConditionWaiter() {
        Node t = lastWaiter;
        // If lastWaiter is cancelled, clean out.
        // 如果尾部的 waiter 不是 CONDITION 状态了,删除
        if (t != null && t.waitStatus != Node.CONDITION) {
            unlinkCancelledWaiters();
            t = lastWaiter;
        }
        // 新建条件队列 node
        Node node = new Node(Thread.currentThread(), Node.CONDITION);
        // 队列是空的,直接放到队列头
        if (t == null)
            firstWaiter = node;
        // 队列不为空,直接到队列尾部
        else
            t.nextWaiter = node;
        lastWaiter = node;
        return node;
    }

    全体のプロセスは比較的単純です, キューに追加するだけです。最後に、unlinkCancelledWaiters という重要なメソッドがあります。このメソッドは、ステータスが CONDITION ではない条件キュー内のすべてのノードを削除します。次のように、unlinkCancelledWaiters メソッドのソース コードを見てみましょう。 :

    2.1.2, unlinkCancelledWaiters
    // 会检查尾部的 waiter 是不是已经不是CONDITION状态了
    // 如果不是,删除这些 waiter
    private void unlinkCancelledWaiters() {
        Node t = firstWaiter;
        // trail 表示上一个状态,这个字段作用非常大,可以把状态都是 CONDITION 的 node 串联起来,即使 node 之间有其他节点都可以
        Node trail = null;
        while (t != null) {
            Node next = t.nextWaiter;
            // 当前node的状态不是CONDITION,删除自己
            if (t.waitStatus != Node.CONDITION) {
                //删除当前node
                t.nextWaiter = null;
                // 如果 trail 是空的,咱们循环又是从头开始的,说明从头到当前节点的状态都不是 CONDITION
                // 都已经被删除了,所以移动队列头节点到当前节点的下一个节点
                if (trail == null)
                    firstWaiter = next;
                // 如果找到上次状态是CONDITION的节点的话,先把当前节点删掉,然后把自己挂到上一个状态是 CONDITION 的节点上
                else
                    trail.nextWaiter = next;
                // 遍历结束,最后一次找到的CONDITION节点就是尾节点
                if (next == null)
                    lastWaiter = trail;
            }
            // 状态是 CONDITION 的 Node
            else
                trail = t;
            // 继续循环,循环顺序从头到尾
            t = next;
        }
    }

    このメソッドを誰でも理解しやすいように、次のような解釈図を描きました。

    2.2. 単一ウェイクアップ信号 Java シンクロナイザー AQS アーキテクチャはどのようにしてロックを解放し、キューを同期しますか

    信号方式はウェイクアップです たとえば、以前キューがいっぱいで、一部のスレッドがテイクのために条件付きキューでブロックされたことを意味します。突然、キュー内の要素がスレッド A によって消費されました。スレッド A は、シグナル メソッドを呼び出して、以前にブロックされていたスレッドをウェイクアップし、スレッドが開始されます。条件付きキューのヘッド ノードがウェイクアップし始めます (「プロセスの全体的なアーキテクチャ図の青い部分) ソース コードは次のとおりです:

    // 唤醒阻塞在条件队列中的节点
    public final void signal() {
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        // 从头节点开始唤醒
        Node first = firstWaiter;
        if (first != null)
            // doSignal 方法会把条件队列中的节点转移到同步队列中去
            doSignal(first);
    }
    // 把条件队列头节点转移到同步队列去
    private void doSignal(Node first) {
        do {
            // nextWaiter为空,说明到队尾了
            if ( (firstWaiter = first.nextWaiter) == null)
                lastWaiter = null;
            // 从队列头部开始唤醒,所以直接把头节点.next 置为 null,这种操作其实就是把 node 从条件队列中移除了
            // 这里有个重要的点是,每次唤醒都是从队列头部开始唤醒,所以把 next 置为 null 没有关系,如果唤醒是从任意节点开始唤醒的话,就会有问题,容易造成链表的割裂
            first.nextWaiter = null;
            // transferForSignal 方法会把节点转移到同步队列中去
            // 通过 while 保证 transferForSignal 能成功
            // 等待队列的 node 不用管他,在 await 的时候,会自动清除状态不是 Condition 的节点(通过 unlinkCancelledWaiters 方法)
            // (first = firstWaiter) != null  = true 的话,表示还可以继续循环, = false 说明队列中的元素已经循环完了
        } while (!transferForSignal(first) &&
                 (first = firstWaiter) != null);
    }

    最も重要なメソッドである transferForSignal を見てみましょう。

    // 返回 true 表示转移成功, false 失败
    // 大概思路:
    // 1. node 追加到同步队列的队尾
    // 2. 将 node 的前一个节点状态置为 SIGNAL,成功直接返回,失败直接唤醒
    // 可以看出来 node 的状态此时是 0 了
    final boolean transferForSignal(Node node) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         */
        // 将 node 的状态从 CONDITION 修改成初始化,失败返回 false
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
        // 当前队列加入到同步队列,返回的 p 是 node 在同步队列中的前一个节点
        // 看命名是 p,实际是 pre 单词的缩写
        Node p = enq(node);
        int ws = p.waitStatus;
        // 状态修改成 SIGNAL,如果成功直接返回
        // 把当前节点的前一个节点修改成 SIGNAL 的原因,是因为 SIGNAL 本身就表示当前节点后面的节点都是需要被唤醒的
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            // 如果 p 节点被取消,或者状态不能修改成SIGNAL,直接唤醒
            LockSupport.unpark(node.thread);
        return true;
    }

    ソース コード全体を読むと、条件キュー内のノードをウェイクアップすると、実際に条件キュー内のノードが同期キューに転送され、前のノードのステータスが SIGNAL に設定されることがわかります。

    2.3. すべての signalAll をウェイクアップする

    signalAll の機能は、条件キュー内のすべてのノードをウェイクアップすることです。ソース コードは次のとおりです。ソースコードから見ると、その本質は次のとおりです。 transferForSignal メソッドがループ内で呼び出され、条件キュー内のノードを同期キューに転送します。

    3. 概要

    AQS ソース コードがついに完成しました。理解できましたか? AQS アーキテクチャ図を黙って思い出して、理解できるかどうかを確認してください。

    Java シンクロナイザー AQS アーキテクチャはどのようにしてロックを解放し、キューを同期しますか

    以上がJava シンクロナイザー AQS アーキテクチャはどのようにしてロックを解放し、キューを同期しますかの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

    声明:
    この記事はyisu.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。