소스 코드 분석 동기 큐, 고유 큐

요약: 동기 큐는 용량이 없는 일종의 고유 큐입니다. 예를 들어 호출자가 큐에 데이터를 넣으면 호출자는 즉시 데이터를 반환할 수 없습니다. 호출자는 다른 사람이 넣어주는 데이터를 기다려야 합니다. 소모, 반환할 수 있습니다.

이 기사는 HUAWEI CLOUD Community " Synchronous Queue Source Code Analysis ", 작성자: JavaEdge에서 공유됩니다.

1. 소개

동기 대기열은 자체 용량이 없는 고유한 대기열입니다. 예를 들어 호출자가 데이터 조각을 대기열에 넣으면 호출자가 즉시 이를 반환할 수 없습니다. 호출자는 내가 넣은 데이터를 다른 사람이 소비할 때까지 기다려야 합니다. , 반환할 수 있습니다. 동기 큐는 MQ에서 널리 사용되며, 이 기사에서는 동기 큐가 이 기능을 소스 코드에서 구현하는 방법을 살펴보겠습니다.

2 전체 아키텍처

AQS를 사용하여 동시성을 달성하는 ArrayBlockingQueue 및 LinkedBlockingDeque와 같은 차단 대기열과 달리 SynchronousQueue는 안전한 데이터 액세스를 달성하기 위해 CAS 작업을 직접 사용하므로 소스 코드가 많은 CAS 코드로 가득 차 있습니다.

SynchronousQueue의 전체 디자인은 비교적 추상적입니다.두 가지 알고리즘 구현이 내부적으로 추상화되어 하나는 선입선출 대기열이고 다른 하나는 후입선출 스택입니다.두 알고리즘은 두 개의 내부 클래스, 그리고 직접 외부 put 및 take 메소드의 구현은 매우 간단합니다.두 내부 클래스의 전송 메소드를 직접 호출하여 구현합니다.전체 호출 관계는 다음 그림과 같습니다.

2.1 클래스 주석

큐는 데이터를 저장하지 않으므로 크기가 없고 반복될 수 없습니다. 삽입 작업의 반환은 다른 스레드가 해당 데이터의 삭제 작업을 완료할 때까지 기다려야 하며 그 반대의 경우도 마찬가지입니다.

대기열은 후입선출 스택과 선입선출 대기열의 두 가지 데이터 구조로 구성되며 스택은 불공평하고 대기열은 공정합니다.

두 번째 요점은 어떻게 이루어지나요? 스택은 어떻게 구현됩니까? 다음에는 차근차근 공개하도록 하겠습니다.

2.2 클래스 다이어그램

SynchronousQueue의 전체 클래스 다이어그램은 LinkedBlockingQueue와 유사하며 둘 다 BlockingQueue 인터페이스를 구현하지만 데이터 구조를 저장하지 않기 때문에 isEmpty, size, contains, remove 및 iteration 메서드와 같은 일부 메서드는 구현되지 않습니다. default. , 다음 스크린샷:

2.3 구조적 세부사항

SynchronousQueue의 기본 구조는 다른 큐와 완전히 다릅니다. 큐와 스택이라는 두 가지 고유한 데이터 구조가 있습니다. 데이터 구조를 살펴보겠습니다.

// 堆栈和队列共同的接口
// 负责执行 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라는 공통 인터페이스를 가지고 있습니다. 이 인터페이스에는 다음과 같은 메서드가 있습니다. 이 메서드는 매우 마술적이며 테이크와 풋의 이중 기능을 수행합니다.

초기화할 때 스택을 사용할지 아니면 큐를 사용할지 선택할 수 있는데 선택하지 않으면 기본값은 스택입니다.이것은 클래스 주석에도 설명되어 있습니다.스택의 효율성은 큐의 효율성보다 높습니다. .

다음으로 스택 및 큐의 특정 구현을 살펴보겠습니다.

3 불공정 스택

3.1 스택의 구조

먼저 스택의 전체 구조를 다음과 같이 소개합니다.

위의 그림에서 볼 수 있듯이 우리는 큰 스택 풀을 가지고 있으며 풀을 여는 것을 스택 헤더라고 하며 넣을 때 스택 풀에 데이터를 넣습니다. 가져갈 때 스택 풀에서 데이터를 가져옵니다. 두 작업 모두 스택 헤드의 데이터에 대해 작동합니다. 그림에서 알 수 있듯이 스택 헤드에 가까울수록 최신 데이터이므로 가져갈 때마다 , 당신은 데이터를 얻을 것입니다. 스택의 선두에 있는 최신 데이터, 이것은 우리가 LIFO라고 부르는 불공평합니다.

그림의 SNode는 소스 코드의 스택 요소를 나타내는 것으로 소스 코드를 살펴보겠습니다.

  • 휘발성 SNode next 의 다음 스택
    은 현재 스택에서 아래로 푸시되는 스택 요소입니다.
  • 휘발성 SNode 일치
    노드 일치, 차단 스택 요소를 깨울 수 있는 타이밍을 결정하는 데 사용
  • 예를 들어 take를 먼저 실행하면 현재 큐에 데이터가 없고 take는 블록되어 있고 stack 요소는 SNode1이고
    put 연산이 있을 때 현재 put의 스택 요소는 할당된다. SNode1의 match 속성, 그리고
    take가 깨어날 때 take 작업이 깨어날 것입니다.
  • 휘발성 스레드 대기자
    스택 요소의 차단은 스레드 차단에 의해 이루어지며 대기자는 차단된 스레드입니다.
  • 개체 항목
    배달되지 않은 메시지 또는 사용되지 않은 메시지

3.2 푸시 앤 팝

  • 스택
    에서 put과 같은 메소드를 사용하여 스택 풀에 데이터를 넣습니다.
  • 스택에서 팝아웃(Pop out of the stack
    ) take와 같은 메소드를 사용하여 스택 풀에서 데이터를 가져옵니다.

작업의 대상은 모두 스택 헤드입니다. 둘 중 하나는 스택 헤드에서 데이터를 가져오고 다른 하나는 데이터를 넣는 것이지만 기본 방법은 동일합니다. 소스 코드는 다음과 같습니다.

전송 방법에 대한 아이디어는 가져오고 넣는 두 가지 방법이 함께 혼합되어 있기 때문에 더 복잡합니다.

@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. 스택 헤더가 비어 있으면 현재 작업을 스택 헤더로 설정하거나 스택 헤더가 비어 있지 않지만 스택 헤더의 작업은 이 작업과 동일하며 현재 작업도 스택 헤더로 설정하고 참조하십시오. 다른 스레드가 자신을 만족시킬 수 있으면 충족되지 않으면 자신을 차단합니다. 예를 들어, 현재 작업은 take이지만 대기열에 데이터가 없으면 자체적으로 차단됩니다.
  5. 스택 헤더가 이미 차단되어 있고 다른 사람이 깨워야 하는 경우 현재 작업이 스택 헤더를 깨울 수 있는지 여부를 확인하려면 깨우고 6으로 이동하고 그렇지 않으면 4로 이동할 수 있습니다.
  6. 자신을 노드로 취급하고 스택 헤더의 일치 속성에 할당하고 스택 헤더 노드를 깨우십시오.
  7. 스택 헤더가 깨어난 후, 그것은 깨어난 노드의 정보를 반환하는 match 속성을 얻습니다.

전체 프로세스에서 노드 차단 방법이 있으며 소스 코드는 다음과 같습니다.

노드/스레드가 막히려고 하면 웨이터 필드를 설정한 다음 실제로 주차하기 전에 상태를 한 번 더 확인하고 레이스와 구현자를 포함하여 웨이터가 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 원소 조성

  • volatile QNode 현재 요소의 다음 다음
    요소
  • volatile Object item // null로 또는 null에서 CAS
    '된 현재 요소의 값, 현재 요소가 차단되면 다른 스레드가 깨어나면 다른 스레드가 항목으로 설정됩니다.
  • volatile 스레드 웨이터 // 파크/언파크를 제어하기
    위해 현재 스레드를 차단할 수 있습니다.
  • final boolean isData
    true이면 넣기, 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) 연산을 수행한다.현재 paired consumer 쓰레드가 없기 때문에 put1 쓰레드는 queue에 들어가 잠시 회전 후 sleep하고 대기한다. 이때 queue 상태는 다음과 같다.

2. 다음으로, put2 쓰레드는 put(2) 연산을 수행하며, 이전과 같이 put2 쓰레드는 queue에 진입하여 잠시 회전한 후 sleep하고 대기하며 이때 queue 상태는 다음과 같다.

3. 이때, take1 쓰레드가 와서 take 연산을 수행하는데 꼬리가 put2 쓰레드를 가리키고 있기 때문에 put2 쓰레드는 take1 쓰레드와 짝을 이룬다(one put, one take) 이때 take1 쓰레드는 대기열에 합류할 필요는 없지만 주의하십시오. 이때 깨우는 스레드는 put2가 아니라 put1입니다.

왜요? 우리가 말하는 것이 공정성 전략이라는 것을 모두가 알아야 합니다. 이른바 공정성이라고 하는 것은 먼저 팀에 합류한 사람이 먼저 깨어난다는 것입니다. 우리의 예는 분명히 put1이 먼저 깨어나야 한다는 것입니다. 일부 학생들은 질문을 할 수 있습니다.take1의 스레드가 put2의 스레드와 일치하는 것이 명백하고 결과는 put1의 스레드가 소비를 위해 깨워집니다.take1의 스레드도 head.next와 일치할 수 있는지 확인하는 방법 마디? 실제로 종이 한 장을 가져다가 그림을 그리면 실제로 이런 모습임을 알게 될 것입니다.

공정한 전략은 다음과 같이 요약될 수 있습니다. 팀의 꼬리가 팀의 머리와 일치합니다.

실행 후 put1 쓰레드가 깨어나고 take1 쓰레드의 take() 메소드가 1(put1 쓰레드의 데이터)을 반환하여 쓰레드간 일대일 통신을 구현한다 이때 내부 상태는 다음과 같다. 다음과 같다:

4. 마지막으로 take 작업을 수행할 또 다른 쓰레드 take2가 있는데, 이때 put2 쓰레드만 대기 중이고 두 쓰레드가 매칭되고, put2 쓰레드가 깨어나고 take2 쓰레드 take 연산은 2(데이터 스레드 put2). 이때 큐는 다음과 같이 시작점으로 돌아갑니다.

위는 공정 모드에서 SynchronousQueue의 구현 모델입니다. 요약하자면, 팀의 꼬리는 팀의 머리와 일치하며, 선입선출은 공정성의 원칙을 반영합니다.

5 불공정 모델

5.1 원소 조성

  • 스택의 맨 위

  • 휘발성 SNode 스택의 다음 다음
    요소
  • volatile Object item // data; 또는 REQUEST
    의 경우 null
  • 휘발성 스레드 웨이터
    는 현재 스레드를 차단할 수 있습니다.

5.2 불공정 모델 설명

또는 페어 모드와 동일한 운영 프로세스를 사용하고 두 전략의 차이점을 비교합니다.

불공정 모드의 기본 구현은 스택인 TransferStack을 사용합니다. 구현에서 헤드 포인터는 스택의 상단을 가리키는 데 사용됩니다. 그런 다음 구현 모델을 살펴보겠습니다.

1. put1 쓰레드는 put(1) 연산을 수행하는데 현재 짝을 이룬 Consumer 쓰레드가 없기 때문에 put1 쓰레드를 스택에 밀어넣고 잠시 회전한 후 sleep 상태로 대기한다. 다음과 같다

2. 그런 다음, put2 쓰레드는 다시 put(2) 연산을 수행한다.전과 같이 put2 쓰레드를 스택에 push하고, 잠시 회전한 후 sleep 상태로 대기한다. 이때 스택 상태는 다음과 같다. :

3. 이때, take1 쓰레드가 와서 take 작업을 수행했는데, 이때 스택의 최상단이 put2 쓰레드인 것을 확인하고 매칭에 성공했지만 구현은 먼저 take1 쓰레드를 push한다. 스택, 그런 다음 take1 스레드가 반복되어 put2 스레드와 일치하는 논리를 실행합니다. 동시성 충돌이 없으면 스택 상단 포인터가 put1 스레드를 직접 가리킵니다.

4. 마지막으로 또 다른 스레드인 take2가 take 연산을 수행하는데 이는 기본적으로 3단계의 로직과 동일하다. 완료되면 스택이 비게 되고 초기 상태가 복원됩니다. ,아래 그림과 같이

위의 과정을 보면 put1 쓰레드가 먼저 스택에 들어가지만 나중에 매칭되는 것이 불공평의 원인임을 알 수 있다.

5 요약

SynchronousQueue의 소스 코드는 비교적 복잡합니다. 소스 코드를 배우기 위해 소스 코드를 디버그하는 것이 좋습니다. 우리는 당신을 위해 디버깅 클래스를 준비했습니다: SynchronousQueueDemo. 소스 코드를 다운로드하고 직접 디버그할 수 있으므로 다음과 같아야 합니다. 배우기 쉽습니다.

  • SynchronousQueue에 요소를 저장할 컨테이너가 없는 이유는 무엇입니까?
    내부에 컨테이너가 없다는 것은 배열과 같이 여러 요소를 저장할 수 있는 메모리 공간이 없지만 데이터를 교환하기 위한 단일 주소 메모리 공간이 있음을 의미합니다.

고유한 스레드 일대일 페어링 통신 메커니즘으로 인해 SynchronousQueue는 대부분의 일반적인 개발에서는 사용되지 않을 수 있지만 스레드 풀 기술에서는 사용될 것입니다. AQS는 내부적으로 사용되지 않으므로 CAS가 직접 사용됩니다. 코드를 이해하기 어렵지만 이것이 우리가 기본 구현 모델을 이해하는 것을 방해하지는 않습니다.모델을 이해하고 소스 코드를 읽는 것을 기반으로 우리는 방향 감각을 갖게되고 더 쉽게 보일 것입니다!

 

HUAWEI CLOUD의 새로운 기술에 대해 처음으로 알아보려면 팔로우를 클릭하세요~

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

추천

출처my.oschina.net/u/4526289/blog/5519603