ソースコード分析同期キュー、一意のキュー

概要: 同期キューは、容量のない一種の一意のキューです。たとえば、発信者がデータをキューに入れると、発信者はすぐにデータを返すことができません。発信者は、他の人が私を入れるデータを待つ必要があります。消費された、あなたは戻ることができます。

この記事は、HUAWEICLOUDコミュニティ「SynchronousQueueSource Code Analysis」、作成者:JavaEdgeから共有されています。

1はじめに

同期キューは一意のキューであり、それ自体には容量がありません。たとえば、呼び出し元がデータをキューに入れると、呼び出し元はすぐにそれを返すことはできません。呼び出し元は、他の人が入力したデータを消費するのを待つ必要があります。 、戻ることができるように。同期キューはMQで広く使用されています。この記事では、同期キューがソースコードからこの関数をどのように実装するかを見てみましょう。

2全体的なアーキテクチャ

AQSを使用して同時実行性を実現するArrayBlockingQueueやLinkedBlockingDequeなどのブロッキングキューとは異なり、SynchronousQueueはCAS操作を直接使用して安全なデータアクセスを実現するため、ソースコードには多くのCASコードが含まれています。

SynchronousQueueの全体的な設計は比較的抽象的です。2つのアルゴリズムの実装は内部で抽象化されています。1つは先入れ先出しのキューで、もう1つは後入れ先出しのスタックです。2つのアルゴリズムは2つの内部で実装されています。クラス、および直接外部putメソッドとtakeメソッドの実装は非常に簡単で、2つの内部クラスの転送メソッドを直接呼び出すことによって実装されます。全体的な呼び出し関係を次の図に示します。

2.1クラスアノテーション

キューはデータを格納しないため、サイズがなく、繰り返すことはできません。挿入操作の戻りは、別のスレッドが対応するデータの削除操作を完了するのを待つ必要があり、その逆も同様です。

キューは、後入れ先出しスタックと先入れ先出しキューの2つのデータ構造で構成されています。スタックは不公平であり、キューは公平です。

2番目のポイントはどのように行われますか?スタックはどのように実装されますか?次に少しずつ公開していきます。

2.2クラス図

SynchronousQueueの全体的なクラス図はLinkedBlockingQueueに似ています。どちらもBlockingQueueインターフェイスを実装していますが、データ構造を格納しないため、isEmpty、size、contains、remove、iterationメソッドなどの一部のメソッドは実装されていません。デフォルト。、次のスクリーンショット:

2.3構造の詳細

SynchronousQueueの基本構造は、他のキューとは完全に異なります。キューとスタックという2つの固有のデータ構造があります。データ構造を見てみましょう。

// 堆栈和队列共同的接口
// 负责执行 put or take
abstract static class Transferer<E> {
    // e 为空的,会直接返回特殊值,不为空会传递给消费者
    // timed 为 true,说明会有超时时间
    abstract E transfer(E e, boolean timed, long nanos);
}

// 堆栈 后入先出 非公平
// Scherer-Scott 算法
static final class TransferStack<E> extends Transferer<E> {
}

// 队列 先入先出 公平
static final class TransferQueue<E> extends Transferer<E> {
}

private transient volatile Transferer<E> transferer;

// 无参构造器默认为非公平的
public SynchronousQueue(boolean fair) {
    transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}

ソースコードから、いくつかのポイントを得ることができます。

スタックとキューの両方に、Transfererと呼ばれる共通のインターフェースがあります。これには次のメソッドがあります。transferは非常に魔法であり、takeとputの2つの機能を実行します。

初期化するときに、スタックとキューのどちらを使用するかを選択できます。選択しない場合、デフォルトはスタックです。これはクラスアノテーションでも説明されています。スタックの効率はキューの効率よりも高くなります。 。

次に、スタックとキューの特定の実装を見てみましょう。

3不公平なスタック

3.1スタックの構造

まず、次のようにスタックの全体的な構造を紹介しましょう。

上の図からわかるように、大きなスタックプールがあり、プールの開口部はスタックヘッダーと呼ばれ、配置されると、データをスタックプールに配置します。取得する場合、データはスタックプールから取得されます。どちらの操作もスタックヘッドのデータに対して実行されます。図からわかるように、スタックヘッドに近いほどデータが新しいため、取得するたびに、データを取得します。スタックの先頭にある最新のデータ、これは私たちがLIFOと呼んでいるものであり、不公平です。

図のSNodeは、ソースコード内のスタック要素を表したものです。ソースコードを見てみましょう。

  • 次の揮発性SNode
    のスタックは、現在のスタックによって下にプッシュされるスタック要素です。
  • 揮発性SNode一致
    ノードマッチング。ブロッキングスタック要素を起動できるタイミングを決定するために使用されます
  • たとえば、最初にtakeを実行すると、この時点ではキューにデータがなく、takeはブロックされ、スタック要素はSNode1になります。put
    操作があると、現在のputのスタック要素がに割り当てられます。 SNode1のmatch属性、およびtakeが起動されるとtake操作が起動さ
    れます。SNode1のmatch属性に値があることがわかった場合、putおよびreturnからデータを取得できます。
  • 揮発性のスレッドウェイター
    スタック要素のブロックはスレッドブロックによって実現され、ウェイターはブロックされたスレッドです
  • オブジェクトアイテムの
    未配信メッセージ、または未消費メッセージ

3.2プッシュアンドポップ

  • スタック上
    putなどのメソッドを使用してデータをスタックプールに配置します
  • スタックからポップアウト
    するtakeなどのメソッドを使用してスタックプールからデータを取り出します

操作の対象はすべてスタックヘッドです。2つのうち1つはスタックヘッドからデータを取得し、もう1つはデータを配置することですが、基本的なメソッドは同じです。ソースコードは次のとおりです。

テイクとプットの2つの方法が混在しているため、転送方法の考え方はより複雑です。

@SuppressWarnings("unchecked")
E transfer(E e, boolean timed, long nanos) {
    SNode s = null; // constructed/reused as needed
 
    // e 为空: take 方法,非空: put 方法
    int mode = (e == null) ? REQUEST : DATA;
 
    // 自旋
    for (;;) {
        // 头节点情况分类
        // 1:为空,说明队列中还没有数据
        // 2:非空,并且是 take 类型的,说明头节点线程正等着拿数据
        // 3:非空,并且是 put 类型的,说明头节点线程正等着放数据
        SNode h = head;
 
        // 栈头为空,说明队列中还没有数据。
        // 栈头非空且栈头的类型和本次操作一致
        //	比如都是 put,那么就把本次 put 操作放到该栈头的前面即可,让本次 put 能够先执行
        if (h == null || h.mode == mode) {  // empty or same-mode
            // 设置了超时时间,并且 e 进栈或者出栈要超时了,
            // 就会丢弃本次操作,返回 null 值。
            // 如果栈头此时被取消了,丢弃栈头,取下一个节点继续消费
            if (timed && nanos <= 0) {      // 无法等待
                // 栈头操作被取消
                if (h != null && h.isCancelled())
                    // 丢弃栈头,把栈头的后一个元素作为栈头
                    casHead(h, h.next);     // 将取消的节点弹栈
                // 栈头为空,直接返回 null
                else
                    return null;
            // 没有超时,直接把 e 作为新的栈头
            } else if (casHead(h, s = snode(s, e, h, mode))) {
                // e 等待出栈,一种是空队列 take,一种是 put
                SNode m = awaitFulfill(s, timed, nanos);
                if (m == s) {               // wait was cancelled
                    clean(s);
                    return null;
                }
                // 本来 s 是栈头的,现在 s 不是栈头了,s 后面又来了一个数,把新的数据作为栈头
                if ((h = head) != null && h.next == s)
                    casHead(h, s.next);     // help s's fulfiller
                return (E) ((mode == REQUEST) ? m.item : s.item);
            }
        // 栈头正在等待其他线程 put 或 take
        // 比如栈头正在阻塞,并且是 put 类型,而此次操作正好是 take 类型,走此处
        } else if (!isFulfilling(h.mode)) { // try to fulfill
            // 栈头已经被取消,把下一个元素作为栈头
            if (h.isCancelled())            // already cancelled
                casHead(h, h.next);         // pop and retry
            // snode 方法第三个参数 h 代表栈头,赋值给 s 的 next 属性
            else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
                for (;;) { // loop until matched or waiters disappear
                    // m 就是栈头,通过上面 snode 方法刚刚赋值
                    SNode m = s.next;       // m is s's match
                    if (m == null) {        // all waiters are gone
                        casHead(s, null);   // pop fulfill node
                        s = null;           // use new node next time
                        break;              // restart main loop
                    }
                    SNode mn = m.next;
                     // tryMatch 非常重要的方法,两个作用:
                     // 1 唤醒被阻塞的栈头 m,2 把当前节点 s 赋值给 m 的 match 属性
                     // 这样栈头 m 被唤醒时,就能从 m.match 中得到本次操作 s
                     // 其中 s.item 记录着本次的操作节点,也就是记录本次操作的数据
                    if (m.tryMatch(s)) {
                        casHead(s, mn);     // pop both s and m
                        return (E) ((mode == REQUEST) ? m.item : s.item);
                    } else                  // lost match
                        s.casNext(m, mn);   // help unlink
                }
            }
        } else {                            // help a fulfiller
            SNode m = h.next;               // m is h's match
            if (m == null)                  // waiter is gone
                casHead(h, null);           // pop fulfilling node
            else {
                SNode mn = m.next;
                if (m.tryMatch(h))          // help match
                    casHead(h, mn);         // pop both h and m
                else                        // lost match
                    h.casNext(m, mn);       // help unlink
            }
        }
    }
}

操作のアイデアを要約すると:

  1. putメソッドかtakeメソッドかを判断します
  2. スタックヘッダーデータが空であるかどうか、空であるか、スタックヘッダーの操作がこの操作と一致しているかどうかを判断します。はいの場合は、3に進み、そうでない場合は5に進みます。
  3. 操作がタイムアウト時間を設定したかどうかを判別します。タイムアウト時間が設定されていてタイムアウトした場合はnullを返し、そうでない場合は4に進みます。
  4. スタックヘッダーが空の場合は、現在の操作をスタックヘッダーに設定するか、スタックヘッダーが空ではないが、スタックヘッダーの操作はこの操作と同じであり、現在の操作もスタックヘッダーに設定します。他のスレッドが自分自身を満足させることができる場合、それが満たされない場合、それは自分自身をブロックします。たとえば、現在の操作は実行されますが、キューにデータがないため、それ自体がブロックされます
  5. スタックヘッダーがすでにブロックされていて、他の誰かがウェイクアップする必要がある場合、現在の操作でスタックヘッダーをウェイクアップできるかどうかを判断するには、ウェイクアップして6に進み、そうでない場合は4に進みます。
  6. 自分をノードとして扱い、スタックヘッダーの一致属性に割り当てて、スタックヘッダーノードをウェイクアップします。
  7. スタックヘッダーが目覚めた後、目覚めたノードの情報を返す一致属性を取得します。

プロセス全体で、ノードをブロックする方法があります。ソースコードは次のとおりです。

ノード/スレッドがブロックしようとすると、ウェイターフィールドを設定し、実際に駐車する前に少なくとももう一度状態をチェックして、レースと実装者をカバーし、ウェイターがnullでないことに注意して、ウェイクアップする必要があります。

呼び出しサイトのスタックの最上位に表示されるノードによって呼び出されると、プロデューサーとコンシューマーが時間内に到着したときにブロックされないように、パークへの呼び出しの前にスピンが実行されます。これは、マルチプロセッサでのみ発生する可能性があります。

メインループから返されるチェックの順序は、優先度:割り込み>通常のリターン>タイムアウトという事実を反映しています。(したがって、タイムアウト時に、あきらめる前に最後の一致チェックが行われます。)時間指定されていないSynchronousQueueからの呼び出しを除きます。{poll / offer}は割り込みをチェックせず、まったく待機しないため、awaitFulfillを呼び出す代わりに、転送メソッドでスタックします。

/**
 * 旋转/阻止,直到节点s通过执行操作匹配。
 * @param s 等待的节点
 * @param timed true if timed wait
 * @param nanos 超时时间
 * @return 匹配的节点, 或者是 s 如果被取消
 */
SNode awaitFulfill(SNode s, boolean timed, long nanos) {
 
    // deadline 死亡时间,如果设置了超时时间的话,死亡时间等于当前时间 + 超时时间,否则就是 0
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    Thread w = Thread.currentThread();
    // 自旋的次数,如果设置了超时时间,会自旋 32 次,否则自旋 512 次。
    // 比如本次操作是 take 操作,自旋次数后,仍无其他线程 put 数据
    // 就会阻塞,有超时时间的,会阻塞固定的时间,否则一致阻塞下去
    int spins = (shouldSpin(s) ?
                 (timed ? maxTimedSpins : maxUntimedSpins) : 0);
    for (;;) {
        // 当前线程有无被打断,如果过了超时时间,当前线程就会被打断
        if (w.isInterrupted())
            s.tryCancel();

        SNode m = s.match;
        if (m != null)
            return m;
        if (timed) {
            nanos = deadline - System.nanoTime();
            // 超时了,取消当前线程的等待操作
            if (nanos <= 0L) {
                s.tryCancel();
                continue;
            }
        }
        // 自选次数减1
        if (spins > 0)
            spins = shouldSpin(s) ? (spins-1) : 0;
        // 把当前线程设置成 waiter,主要是通过线程来完成阻塞和唤醒
        else if (s.waiter == null)
            s.waiter = w; // establish waiter so can park next iter
        else if (!timed)
            // 通过 park 进行阻塞,这个我们在锁章节中会说明
            LockSupport.park(this);
        else if (nanos > spinForTimeoutThreshold)
            LockSupport.parkNanos(this, nanos);
    }
}

そのブロック戦略は、起動するとすぐにはブロックされませんが、特定の回数スピンした後、要件を満たす他のスレッドがまだない場合は本当にブロックされます。

キューの実装戦略は、通常、フェアモードと非フェアモードに分けられます。このホワイトペーパーでは、フェアモードに焦点を当てます。

4均等化キューイング

4.1元素組成


  • 現在の要素の次の次の要素の揮発性QNode
  • volatile Object item // nullへのCASまたはnullからのCAS
    現在の要素の値。現在の要素がブロックされている場合、他のスレッドがウェイクアップすると、他のスレッドがアイテムに設定されます。
  • volatile Threadwaiter//パーク/アンパークを制御
    して現在のスレッドをブロックできます
  • 最終ブールisDatatrue
    が配置され、falseが取得されます

均等化キューは、主にTransferQueueの内部クラスの転送メソッドを使用します。ソースコードを参照してください。

E transfer(E e, boolean timed, long nanos) {

    QNode s = null; // constructed/reused as needed
    // true : put false : get
    boolean isData = (e != null);

    for (;;) {
        // 队列头和尾的临时变量,队列是空的时候,t=h
        QNode t = tail;
        QNode h = head;
        // tail 和 head 没有初始化时,无限循环
        // 虽然这种 continue 非常耗cpu,但感觉不会碰到这种情况
        // 因为 tail 和 head 在 TransferQueue 初始化的时候,就已经被赋值空节点了
        if (t == null || h == null)
            continue;
        // 首尾节点相同,说明是空队列
        // 或者尾节点的操作和当前节点操作一致
        if (h == t || t.isData == isData) {
            QNode tn = t.next;
            // 当 t 不是 tail 时,说明 tail 已经被修改过了
            // 因为 tail 没有被修改的情况下,t 和 tail 必然相等
            // 因为前面刚刚执行赋值操作: t = tail
            if (t != tail)
                continue;
            // 队尾后面的值还不为空,t 还不是队尾,直接把 tn 赋值给 t,这是一步加强校验。
            if (tn != null) {
                advanceTail(t, tn);
                continue;
            }
            //超时直接返回 null
            if (timed && nanos <= 0)        // can't wait
                return null;
            //构造node节点
            if (s == null)
                s = new QNode(e, isData);
            //如果把 e 放到队尾失败,继续递归放进去
            if (!t.casNext(null, s))        // failed to link in
                continue;

            advanceTail(t, s);              // swing tail and wait
            // 阻塞住自己
            Object x = awaitFulfill(s, e, timed, nanos);
            if (x == s) {                   // wait was cancelled
                clean(t, s);
                return null;
            }

            if (!s.isOffList()) {           // not already unlinked
                advanceHead(t, s);          // unlink if head
                if (x != null)              // and forget fields
                    s.item = s;
                s.waiter = null;
            }
            return (x != null) ? (E)x : e;
        // 队列不为空,并且当前操作和队尾不一致
        // 也就是说当前操作是队尾是对应的操作
        // 比如说队尾是因为 take 被阻塞的,那么当前操作必然是 put
        } else {                            // complementary-mode
            // 如果是第一次执行,此处的 m 代表就是 tail
            // 也就是这行代码体现出队列的公平,每次操作时,从头开始按照顺序进行操作
            QNode m = h.next;               // node to fulfill
            if (t != tail || m == null || h != head)
                continue;                   // inconsistent read

            Object x = m.item;
            if (isData == (x != null) ||    // m already fulfilled
                x == m ||                   // m cancelled
                // m 代表栈头
                // 这里把当前的操作值赋值给阻塞住的 m 的 item 属性
                // 这样 m 被释放时,就可得到此次操作的值
                !m.casItem(x, e)) {         // lost CAS
                advanceHead(h, m);          // dequeue and retry
                continue;
            }
            // 当前操作放到队头
            advanceHead(h, m);              // successfully fulfilled
            // 释放队头阻塞节点
            LockSupport.unpark(m.waiter);
            return (x != null) ? (E)x : e;
        }
    }
}

スレッドがブロックされた後、現在のスレッドはどのようにして自身のデータをブロックされたスレッドに渡しますか。

スレッド1がキューからデータを取得してブロックされたとすると、スレッドAはブロックされ、スレッド2はデータBをキューに入れ始めます。一般的なプロセスは次のとおりです。

  • スレッド1はキューからデータを取得し、キューにデータがないことを検出したため、ブロックされてAになります
  • スレッド2がデータをキューの最後に置くと、キューの最後から最初にブロックされたノードが見つかります。この時点でノードAが見つかったとすると、スレッドBは入れたデータを次のアイテム属性に入れます。ノードA、およびウェイクアップスレッド1
  • スレッド1が起動した後、スレッド2によって配置されたデータをA.itemから取得でき、スレッド1は正常に戻ります。

このプロセスでは、公平性は主に、データが配置されるたびにチームのテールに配置され、データが取得されるたびに、パイルの先頭から直接取得されるのではなく、チームは最初のものを見つけます。ブロックされたスレッド。これにより、ブロックされたスレッドが順番に解放されます。

4.2グラフィカルな均等化キューイングモデル

フェアモードでは、基盤となる実装はTransferQueueキューを使用します。このキューには、現在一致を待機しているスレッドノードを指すヘッドポインターとテールポインターがあります。

初期化されると、TransferQueueのステータスは次のようになります。

1.スレッドput1はput(1)操作を実行します。現在、ペアのコンシューマスレッドがないため、put1スレッドはキューに入り、しばらくスピンした後、スリープして待機します。このとき、キューのステータスは次のようになります。

2.次に、スレッドput2がput(2)操作を実行します。前と同じように、put2スレッドはキューに入り、しばらくスピンしてからスリープして待機します。このとき、キューのステータスは次のようになります。

3.このとき、スレッドtake1が来て、take操作を実行しました。テールがput2スレッドを指しているため、put2スレッドはtake1スレッドとペアになっています(1つのputと1つのtake)。このとき、take1スレッドはキューに参加する必要はありませんが、注意してください。このとき、ウェイクアップするスレッドはput2ではなくput1です。

なんで?私たちが話しているのは公平性戦略であることを誰もが知っている必要があります。いわゆる公平性とは、チームに最初に参加した人が最初に目覚めるということです。この例では、明らかにput1を最初に目覚めさせる必要があります。一部の学生は質問をするかもしれません。take1のスレッドがput2のスレッドと一致することは明らかであり、その結果、put1のスレッドが消費のために目覚めます。take1のスレッドもhead.nextと一致することを確認する方法ノード?実際、一枚の紙を撮って絵を描くことができ、それが本当にこのようなものであることがわかります。

公正な戦略は次のように要約できます。チームのテールはチームのヘッドと一致します。

実行後、put1スレッドが起動され、take1スレッドのtake()メソッドが1(put1スレッドのデータ)を返すことで、スレッド間の1対1の通信を実現します。このときの内部状態は次のようになります。次のとおりです。

4.最後に、take操作を実行する別のスレッドtake2があります。このとき、put2スレッドのみが待機しており、2つのスレッドが一致し、スレッドput2が起動され、take2スレッドtake操作が2を返します(データスレッドput2の)。この時点で、キューは次のように開始点に戻ります。

上記は、フェアモードでのSynchronousQueueの実装モデルです。要約すると、チームのテールは、公平性の原則を反映して、先入れ先出しでチームのヘッドと一致します。

5つの不公平なモデル

5.1元素組成

  • スタックのトップ

  • スタック内の揮発性SNodeの次の次の
    要素
  • volatile Object item // data;またはREQUESTの場合はnull
    現在の要素の値。現在の要素がブロックされている場合、他のスレッドがウェイクアップすると、他のスレッドがアイテムに設定されます。
  • 揮発性スレッドウェイター
    は現在のスレッドをブロックできます

5.2不公平なモデルの説明

または、フェアモードと同じ操作プロセスを使用して、2つの戦略の違いを比較します。

不公平モードの基本的な実装では、スタックであるTransferStackを使用します。実装では、ヘッドポインターを使用してスタックの最上位を指します。次に、その実装モデルを見てみましょう。

1.スレッドput1はput(1)操作を実行します。現在、ペアのコンシューマスレッドがないため、put1スレッドはスタックにプッシュされ、しばらくスピンした後、スリープして待機します。このとき、スタックの状態以下のとおりであります

2.次に、スレッドput2が再びput(2)操作を実行します。前と同じように、put2スレッドはスタックにプッシュされ、しばらく回転した後、スリープして待機します。このとき、スタックの状態は次のようになります。 :

3.このとき、スレッドtake1が来て、take操作を実行しました。このとき、スタックの一番上がput2スレッドであることがわかり、一致は成功しましたが、実装は最初にtake1スレッドをにプッシュします。スタック、次にtake1スレッドがループして、put2スレッドに一致するロジックを実行します。同時実行の競合がない場合、スタックトップポインターはput1スレッドを直接指します。

4.最後に、別のスレッドtake2がtake操作を実行します。これは、基本的に手順3のロジックと同じです。take2スレッドがスタックにプッシュされ、put1スレッドがループで照合されます。最後に、すべてが照合されます。が完了すると、スタックが空になり、初期状態が復元されます。、以下に示すように:

上記のプロセスから、put1スレッドは最初にスタックにプッシュされますが、後で一致することがわかります。これが不公平の原因です。

5まとめ

SynchronousQueueのソースコードは比較的複雑です。ソースコードをデバッグしてソースコードを学習することをお勧めします。SynchronousQueueDemoというデバッグクラスを用意しています。ソースコードをダウンロードして自分でデバッグできるので、次のようにする必要があります。習得が容易です。

  • SynchronousQueueに要素を格納するためのコンテナがないのはなぜですか?
    内部にコンテナがないということは、複数の要素を格納するための配列のようなメモリスペースがないことを意味しますが、データを交換するための単一のアドレスメモリスペースがあります

SynchronousQueueは、独自のスレッド1対1ペアリング通信メカニズムにより、通常の開発では使用できませんが、スレッドプールテクノロジーで使用されます。AQSは内部で使用されないため、CASが直接使用されます。コードを理解するのは難しいですが、これは基礎となる実装モデルを理解することを妨げるものではありません。モデルを理解し、ソースコードを読むことに基づいて、方向性がわかり、見やすくなります。

 

[フォロー]をクリックして、HUAWEI CLOUDの新技術について初めて学びましょう〜

{{o.name}}
{{m.name}}

おすすめ

転載: my.oschina.net/u/4526289/blog/5519603