Javaのマルチスレッドと並列性(VI):AQS

我々はいくつかの、JUCセキュリティスレッドを確保するために、CASと揮発性を使用することですクラスの基本的なツールキットは、JUCクラス内のパッケージ全体が彼らのビルドに基づいている前述しました。今日は非常に重要なシンクロナイザを紹介し、このクラスは、私たちにのCASと揮発性に基づいて同期ツールJDKクラスです。

背景

これらのコンポーネントは、直列同期AbstractQueuedSynchronizer、並行性成分カラムのためのいくつかのサポートを提供するJDK 1.5を導入JUCパッケージであり、それらは主に以下の機能です。

  • 状態は、ロックが取得または解放される表すような、管理し、内部状態を更新します。
  • スレッド同期のステータスをブロックします。
  • スレッドの同期状態をリリースしました。

AQSは、小さなフレームで、このフレームワークに基づいて、我々は、シンクロナイザの多くを達成することができ、ReentrantLockの、たCountDownLatch、セマフォなどAQSの実装は基づいています。

機能

  • 排他ロック:1つのスレッドだけReentrantLockのが排他的にミューテックスを実装されている証明するために、このような皆の前のように、ロックを保持することができます。
  • 共有ロック:、複数のスレッドが同時にロックを獲得するようReentrantReadWriteLockなどの共有リソースへの同時アクセスを可能にします。

デザインのアイデア

コアが取得方法同期及びリリース動作です。

取得

(ながら同期の現在状態が {取得動作を許可しません)

現在のスレッドがもはやキューである場合は、キューが追加されません

現在のスレッドをブロック

}

スレッドがデキューされるキューである場合

解除

更新シンクロナイザ状態

(新しい状態は、成功のためにブロックされたスレッドを可能にする)場合

1つ以上のスレッドのキューのブロックを解除。

キューを挿入すると削除、同期ステータスの変更、スレッドブロックおよびリリース:上記のアイデアの操作から、我々は3つの主要なアクションを提案することができます。これは、3つの基本要素から出てくることができます。

  • 同期状態管理原子
  • スレッドのブロックと解除
  • キューの管理

同期状態が
読み出され、同期ステータスを更新するため、および露出するgetStateを、SETSTATEとのcompareAndSet操作をストア同期状態に使用される整数型の値をAQS。スレッド(プラス/マイナス指定量)を変更することによって、コードは、現在のスレッドが正常にうまく同期状態を取得したか否かを決定するかどうか。

状態は、視認性と秩序を確保するために、揮発性になると宣言されます。CASは、のcompareAndSetの命令によって実現することがターン同期状態が一貫した期待時間を有する場合にのみ、このように原子の同期状態を確保する、新しい値原子に設定されるように。

ブロックされた
JSR166まで、スレッドをブロックされ、スレッドブロックはJavaの組み込みのチューブ側に基づいてい持ち上げます。

JUCパッケージには、この問題を解決するためにLockSupportクラスを使用しています。LockSupport.parkブロックはLockSupport.unpark方法まで、現在のスレッドが呼び出されます。

キュー
フレームワークのコアがスレッドを管理する方法であるがキューにブロックされ、待ち行列は、厳密にFIFOキューであるため、同期のスレッドの優先順位をサポートしていません。同期キュー自体のための最良の選択は、非ブロッキングロック基礎となるデータ構造を構築するために使用されていません。ここでは、CLHロックを採用しました。

CLHは、それがチームだし、チームは密接に実際のビジネスに関連して、実際のキューキューのようにそれほどではありません。これは、リンクリストキューです。ヘッドAQSがアクセスする二つのフィールド(ヘッドノード)と尾部(テール・ノード)を使用し、これら二つのフィールドの初期化は、空のノードを指します。
エンキュー操作:

CLHキューはFIFOキューであるので、新しいノードが来て、後に現在のキューノードの最後に挿入されます。スレッドが同期した状態になると、他のスレッドがノードに追加順番に得ることができず、キューを同期するように構成されており、このプロセスは非常にCAS方式を使用し、スレッドセーフを確保するためにキューに参加する必要があり、それが現在のスレッドに合格する必要性を信じていますテール・ノードと現在のノードは、正式にノード協会の終了前に、現在のノードのみの成功の後に設定しました。

デキュー操作:

それはFIFOキューであるため、彼らは成功しAQS同期状態を取得することができます同期状態の解除時には最初のノード、スレッドのヘッドノードでなければなりません、次のノードを覚ますだろう、ととしての地位を設定すると、後続のノードは、AQSの同期状態の成功を取得します最初のポイント。最初のノードは、完全に成功した取得スレッド同期なので、チーム内のようなノーCAS操作により設定されています。

条件キュー

AQS同期キュー上で、キューは、このセクションの状態です。AQSは1つの同期キュー、キューは、複数の条件を有することができます。AQSは、使用するクラスの排他的同期及びロックインターフェースの実装クラスを維持するために、フレームワークConditionObjectのクラスを提供します。

ConditionObjectのクラスとAQSは、独自の個別のキュー条件を内部ノードを共有しました。葛動作が同期状態にキューから転送キューノードによって達成されます。
新葛:

のawait:

メソッドの構造

パッケージ データの構造
同期ステータス 揮発性のint型の状態
おもり LockSupportクラス
キュー ノードノード
条件キュー ConditionObjectの

ソース

私たちは解放し、利用できる排他的な同期ステータスを通じて、同様のリリースと共有AQS同期状態を達成する方法を見てもらいます。

独占式

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

符号の同期捕捉は、主状態、同期キューとキュースピン待機同期および他の関連作業に追加されたノードの構造を完成させました。

  1. tryAcquire方法は、セキュリティ・スレッドを同時に取得同期ステータスを確保するための方法のサブクラスの実装と呼ばれています。
  2. 状態同期を獲得することは、排他的な同期ノード構成を失敗しました。
  3. これは、ノードaddWriter同期によってキューの末尾に追加されました。
  4. 最後に、ノードが同期状態管理方法を得るために、そのような方法を、acquireQueued。

キューに参加するノード構造を見て、達成:

private Node addWaiter(Node mode) {
        // 当前线程构造成Node节点
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        // 尝试快速在尾节点后新增节点 提升算法效率 先将尾节点指向pred
        Node pred = tail;
        if (pred != null) {
            //尾节点不为空  当前线程节点的前驱节点指向尾节点
            node.prev = pred;
            //并发处理 尾节点有可能已经不是之前的节点 所以需要CAS更新
            if (compareAndSetTail(pred, node)) {
                //CAS更新成功 当前线程为尾节点 原先尾节点的后续节点就是当前节点
                pred.next = node;
                return node;
            }
        }
        //第一个入队的节点或者是尾节点后续节点新增失败时进入enq
        enq(node);
        return node;
    }
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;
                }
            }
        }
    }

ノード同期キューを入力した後、プロセスは条件が満たされた観測されたスピン、各ノードをスレッド、スピンに入り、同期状態へのアクセス、あなたはそれ以外の場合は、まだ、スピンプロセスからスピンを撤回することができます。

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);
    }
}

ShouldParkAfterFailedAcquireプロセスとparkAndCheckInterruptは、スレッドをブロックされています。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //前驱节点的状态决定后续节点的行为
     int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*前驱节点为-1 后续节点可以被阻塞
             * 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 {
            /*前驱节点是初始或者共享状态就设置为-1 使后续节点阻塞
             * 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;
    }
private final boolean parkAndCheckInterrupt() {
        //阻塞线程
        LockSupport.park(this);
        return Thread.interrupted();
    }


正常に同期ステータスを取得した後、同時ロックこのコンポーネントは、現在のスレッドがロックを取得することを意味します。

再看 release 方法:

head节点表示获取锁成功的节点,当头结点在释放同步状态时,会唤醒后继节点,如果后继节点获得锁成功,会把自己设置为头结点,节点的变化过程如下。修改head节点指向下一个获得锁的节点,新的获得锁的节点,将prev的指针指向null。

public final boolean release(int arg) {
        if (tryRelease(arg)) {//同步状态释放成功
            Node h = head;
            if (h != null && h.waitStatus != 0)
                //直接释放头节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
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);
    }

总结:在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中进行自旋。移除的条件是前驱节点是头节点并且成功获取了同步状态。释放时,会唤醒头节点的后继节点。

应用

ReentrantLock:ReentrantLock 类使用 AQS 同步状态来保存锁重复持有的次数。当锁被一个线程获取时,ReentrantLock 也会记录下当前获得锁的线程表示,以便检查是否重复获取。

ReentrantReadWriteLock:ReentrantReadWriteLock 使用 AQS 同步状态中的 16 为来保存写锁的持有次数,剩下的 16 为来保存读锁的持有次数。WriteLock 的构建方式和 ReentrantLock 一样。ReadLock 则通过使用 acquireShared 方法来支持同时允许多个读线程。

Semaphore:信号量使用 AQS 同步状态来保存信号量当前计数。它里面定义的 acquireShared 方法会减少计数,当计数为非正值时阻塞线程。tryRelease 会增加技术,在计数为正值时还要解除线程的阻塞。

CountDownLatch:使用 AQS 同步状态来表示计数。当该计数为 0 时,所有的 acquire 方法才能通过。

FutureTask:使用 AQS 的同步状态来表示某个异步计算任务的运行状态(初始化,运行中,被取消和完成)。设置(FutureTask 的 set 方法)或取消(FutureTask 的 cancel 方法)一个 FutureTask 时会调用 AQS 的 release 操作。等待计算结果的线程阻塞解除是通过 AQS 的 acquire 实现的。

SynchronousQueues:SynchronousQueues类使用了内部的等待节点,这些节点可以用于协调生产者和消费者。同时,它使用AQS同步状态来控制当某个消费者消费当前一项时,允许一个生产者继续生产,反之亦然。

流程图

  1. 多线程并发修改同步状态,修改成功的线程标记为拥有同步状态。

  2. 获取失败的线程,加入到同步队列的队尾;加入到队列中后,如果当前节点的前驱节点为头节点再次尝试获取同步状态(下文代码:p == head && tryAcquire(arg))。

  3. 如果头节点的下一个节点尝试获取同步状态失败后,会进入等待状态;其他节点则继续自旋。

  4. 当线程执行完相应逻辑后,需要释放同步状态,使后继节点有机会同步状态(让出资源,让排队的线程使用)。这时就需要调用release(int arg)方法。调用该方法后,会唤醒后继节点。

  5. 后继节点获取同步状态成功,头节点出队。需要注意的事,出队操作是间接的,有节点获取到同步状态时,会将当前节点设置为head,而原本的head设置为null。

  6. 当同步队列中头节点唤醒后继节点时,此时可能有其他线程尝试获取同步状态。

  7. 假设获取成功,将会被设置为头节点。

  8. 头节点后续节点获取同步状态失败。

  9. 共享模式和独占模式最主要的区别是在支持同一时刻有多个线程同时获取同步状态。为了避免带来额外的负担,在上文中提到的同步队列中都是用独占模式进行讲述,其实同步队列中的节点应该是独占和共享节点并存的。

  10. 共享节点尝试获取同步状态。

  11. 当一个同享节点获取到同步状态,并唤醒后面等待的共享状态的结果如下图所示:

  12. 最后,获取到同步状态的线程执行完毕,同步队列中只有一个独占节点:

总结

  1. AQS通过一个int同步状态码,和一个(先进先出)队列来控制多个线程访问资源
  2. 支持独占和共享两种模式获取同步状态码
  3. 当线程获取同步状态失败会被加入到同步队列中
  4. 当线程释放同步状态,会唤醒后继节点来获取同步状态
  5. 共享模式下的节点获取到同步状态或者释放同步状态时,不仅会唤醒后继节点,还会向后传播,唤醒所有同步节点
  6. 使用volatile关键字保证状态码在线程间的可见性,CAS操作保证修改状态码过程的原子性。

おすすめ

転載: www.cnblogs.com/paulwang92115/p/12168023.html