Java并发编程之SynchronousQueue阻塞队列详解

简介

SynchronousQueue是一个不存储元素的队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。

它支持公平访问队列。默认情况下线程采用非公平性策略访问队列。SynchronousQueue类只有两个构造方法:

public SynchronousQueue() {
    this(false);
}

public SynchronousQueue(boolean fair) {
    transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}

使用第二个构造方法可以创建公平性访问的SynchronousQueue,如果设置为true,则等待的线程会采用先进先出的顺序访问队列。

SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合传递性场景。SynchronousQueue的吞吐量高于ArrayBlockingQueue和LinkedBlockingQueue。

SynchronousQueue源码详解

SynchronousQueue类的定义如下:

public class SynchronousQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable

该类同样继承自AbstractQueue抽象类,并实现了BlockingQueue接口,这里不再叙述。

SynchronousQueue类使用了一个非常关键的内部类来转移数据:

abstract static class Transferer<E> {
    /**
        * Performs a put or take.
        *
        * @param e if non-null, the item to be handed to a consumer;
        *          if null, requests that transfer return an item
        *          offered by producer.
        * @param timed if this operation should timeout
        * @param nanos the timeout, in nanoseconds
        * @return if non-null, the item provided or received; if null,
        *         the operation failed due to timeout or interrupt --
        *         the caller can distinguish which of these occurred
        *         by checking Thread.interrupted.
        */
    abstract E transfer(E e, boolean timed, long nanos);
}

从注释中可以看出,该类的唯一一个transfer方法是通过参数e来区分调用方法的是一个生产者线程还是一个消费者线程,如果e为null,则说明这是一个消费者线程,比如一个take操作,如果e不为null,那么就是一个生产者线程,这个数据就是这个线程需要交付的数据,比如一个put操作。

SynchronousQueue采用队列TransferQueue来实现公平性策略,采用堆栈TransferStack来实现非公平性策略,SynchronousQueue的put、take操作都是委托这两个类来实现的,我们下面先来了解一下这两个类。

TransferQueue

TransferQueue继承自Transferer:

static final class TransferQueue<E> extends Transferer<E>

它使用队列作为交易媒介,来实现公平交易,TransferQueue使用QNode类来作为队列节点:

static final class QNode {
    // 指向下一个节点
    volatile QNode next;          // next node in queue
    // item数据项
    volatile Object item;         // CAS'ed to or from null
    // 等待线程
    volatile Thread waiter;       // to control park/unpark
    // 是否为数据的标识
    final boolean isData;

    ...
}

TransferQueue类中主要有3个QNode的对象:

/** Head of queue */
transient volatile QNode head;
/** Tail of queue */
transient volatile QNode tail;
/**
* Reference to a cancelled node that might not yet have been
* unlinked from queue because it was the last inserted node
* when it was cancelled.
*/
transient volatile QNode cleanMe;

  • head:队列首节点
  • tail:队列尾节点
  • cleanMe:指向一个被取消但是还没有从队列移除的节点

同时,对于TransferQueue需要注意的是,其队列永远都存在一个dummy node,在构造时创建:

TransferQueue() {
    QNode h = new QNode(null, false); // initialize to dummy node.
    head = h;
    tail = h;
}

TransferStack

TransferStack同样继承自Transferer:

static final class TransferStack<E> extends Transfer

它使用栈作为交易媒介,来实现非公平交易,TransferStack使用SNode类来作为栈节点:

static final class SNode {
    // 指向栈中的下一个节点
    volatile SNode next;        // next node in stack
    // 匹配节点
    volatile SNode match;       // the node matched to this
    // 等待线程
    volatile Thread waiter;     // to control park/unpark
    // item数据线
    Object item;                // data; or null for REQUESTs
    // 节点状态
    int mode;

    ...
}

节点主要有以下几种状态:

/** Node represents an unfulfilled consumer */
static final int REQUEST    = 0;
/** Node represents an unfulfilled producer */
static final int DATA       = 1;
/** Node is fulfilling another unfulfilled DATA or REQUEST */
static final int FULFILLING = 2;

  • REQUEST:表示了一个请求交易但是没有得到匹配的消费者
  • DATA:表示一个请求交易但是没有交付数据的生产者
  • FULFILLING:表示正在进行交易的生产者或者消费者

SynchronousQueue的put、take操作都是调用TransferQueue或者TransferStack的transfer方法来实现的,我们先来看一下这两个方法:

public void put(E e) throws InterruptedException {
    // 若插入的数据是null,则直接抛出NullPointerException异常
    if (e == null) throw new NullPointerException();
    // 调用transfer方法
    if (transferer.transfer(e, false, 0) == null) {
        Thread.interrupted();
        throw new InterruptedException();
    }
}
public E take() throws InterruptedException {
    // 调用transfer方法
    E e = transferer.transfer(null, false, 0);
    // 若值不为null,则直接返回
    if (e != null)
        return e;
    Thread.interrupted();
    throw new InterruptedException();
}

从源码中可以看到,这两个方法都会调用transfer方法,其中,put方法传递的是e参数,所以模式为数据(公平isData = true,非公平mode= DATA),而take方法传递的是null,所以模式为请求(公平isData = false,非公平mode = REQUEST)。我们下面看一看在公平与非公平模式下的transfer方法具体实现。

公平模式

TransferQueue实现的transfer方法如下:

E transfer(E e, boolean timed, long nanos) {
    QNode s = null; // constructed/reused as needed
    // 获取当前节点的模式
    boolean isData = (e != null);

    for (;;) {
        QNode t = tail;
        QNode h = head;
        // 队列没有初始化,自旋
        if (t == null || h == null)         // saw uninitialized value
            continue;                       // spin

        // 头尾节点相等(队列为null),或者当前节点和队列尾节点具有相同的交易类型
        // 将节点添加到队列尾部,并且等待匹配
        if (h == t || t.isData == isData) { // empty or same-mode
            QNode tn = t.next;
            // t != tail表明已有其他线程修改了tail,当前线程需要重新再来
            if (t != tail)                  // inconsistent read
                continue;
            // 若尾节点的后继节点不为null,则表明已经有其他线程添加了节点,更新尾节点
            if (tn != null) {               // lagging tail
                advanceTail(t, tn);
                continue;
            }
            // 超时
            if (timed && nanos <= 0)        // can't wait
                return null;
            // s == null,则创建一个新节点
            if (s == null)
                s = new QNode(e, isData);
            // 将新节点加入到队列中,如果不成功,继续处理
            if (!t.casNext(null, s))        // failed to link in
                continue;

            // 更新尾节点
            advanceTail(t, s);              // swing tail and wait
            
            // 调用awaitFulfill方法,若节点是head.next,则进行自旋
            // 否则,直接阻塞,直到有其他线程与之匹配,或它自己进行线程的中断
            Object x = awaitFulfill(s, e, timed, nanos);
            // 若返回的x == s表示,当前线程已经超时或者中断,不然的话s == null或者是匹配的节点
            if (x == s) {                   // wait was cancelled
                clean(t, s);
                return null;
            }

            // 若s节点还没有从队列删除
            if (!s.isOffList()) {           // not already unlinked
                // 尝试将s节点设置为head,移出t
                advanceHead(t, s);          // unlink if head
                if (x != null)              // and forget fields
                    s.item = s;
                s.waiter = null;
            }
            return (x != null) ? (E)x : e;

        } 
        // 这里是从head.next开始,因为TransferQueue总是会存在一个dummy节点
        else {                            // complementary-mode
            QNode m = h.next;               // node to fulfill
            // 不一致读,表明有其他线程修改了队列
            if (t != tail || m == null || h != head)
                continue;                   // inconsistent read

            Object x = m.item;
            // isData == (x != null):判断isData与x的模式是否相同,相同表示已经匹配了
            // x == m :m节点被取消了
            // !m.casItem(x, e):如果尝试将数据e设置到m上失败
            if (isData == (x != null) ||    // m already fulfilled
                x == m ||                   // m cancelled
                !m.casItem(x, e)) {         // lost CAS
                // 将m设置为头结点,h出列,然后重试
                advanceHead(h, m);          // dequeue and retry
                continue;
            }

            // 成功匹配了,m设置为头结点h出列,向前推进
            advanceHead(h, m);              // successfully fulfilled
            // 唤醒m的等待线程
            LockSupport.unpark(m.waiter);
            return (x != null) ? (E)x : e;
        }
    }
}

该方法的主要运行过程如下:

1、如果队列为空,或者请求交易的节点和队列中的节点具有相同的交易类型,那么就将该请求交易的节点添加到队列尾部等待交易,直到被匹配或者被取消。

2、如果队列中包含了等待的节点,并且请求的节点和等待的节点是互补的,那么进行匹配并且进行交易。

当队列为空时,节点入列然后通过调用awaitFulfill()方法自旋,该方法主要用于自旋/阻塞节点,直到节点被匹配返回或者取消、中断:

Object awaitFulfill(QNode s, E e, boolean timed, long nanos) {
    /* Same idea as TransferStack.awaitFulfill */
    // 计算超时时间点
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    // 获取当前线程
    Thread w = Thread.currentThread();
    // 自旋次数
    int spins = ((head.next == s) ?
                    (timed ? maxTimedSpins : maxUntimedSpins) : 0);
    // 自旋
    for (;;) {
        // 线程被中断了,取消当前节点
        if (w.isInterrupted())
            s.tryCancel(e);
        // 如果线程进行了阻塞 -> 唤醒或者中断了,那么x != e 肯定成立,直接返回当前节点即可
        Object x = s.item;
        if (x != e)
            return x;
        // 超时判断
        if (timed) {
            nanos = deadline - System.nanoTime();
            // 已超时
            if (nanos <= 0L) {
                s.tryCancel(e);
                continue;
            }
        }
        if (spins > 0)
            --spins;
        // 设置等待线程
        else if (s.waiter == null)
            s.waiter = w;
        // 设置没有超时地阻塞线程
        else if (!timed)
            LockSupport.park(this);
        // 设置具有超时地阻塞线程
        else if (nanos > spinForTimeoutThreshold)
            LockSupport.parkNanos(this, nanos);
    }
}

在自旋/阻塞过程中做了一点优化,就是判断当前节点是否为对头元素,如果是的则先自旋,如果自旋次数过了,则才阻塞,这样做的主要目的就在如果生产者、消费者立马来匹配了则不需要阻塞,因为阻塞、唤醒会消耗资源。在整个自旋的过程中会不断判断是否超时或者中断了,如果中断或者超时了则调用tryCancel()取消该节点。

void tryCancel(Object cmp) {
    UNSAFE.compareAndSwapObject(this, itemOffset, cmp, this);
}

取消过程就是将节点的item设置为自身(itemOffset是item的偏移量)。所以在调用awaitFulfill()方法时,如果当前线程被取消、中断、超时了那么返回的值肯定是s,否则返回的则是匹配的节点。如果返回值是节点s,那么if(x == s)必定成立,如下:

if (x == s) {  // wait was cancelled
    clean(t, s);
    return null;
}

如果返回的x == s成立,则调用clean()方法清理节点s:

void clean(QNode pred, QNode s) {
    s.waiter = null; // forget thread
    /*
        * At any given time, exactly one node on list cannot be
        * deleted -- the last inserted node. To accommodate this,
        * if we cannot delete s, we save its predecessor as
        * "cleanMe", deleting the previously saved version
        * first. At least one of node s or the node previously
        * saved can always be deleted, so this always terminates.
        */
    while (pred.next == s) { // Return early if already unlinked
        QNode h = head;
        QNode hn = h.next;   // Absorb cancelled first node as head
        if (hn != null && hn.isCancelled()) {
            advanceHead(h, hn);
            continue;
        }
        QNode t = tail;      // Ensure consistent read for tail
        if (t == h)
            return;
        QNode tn = t.next;
        if (t != tail)
            continue;
        if (tn != null) {
            advanceTail(t, tn);
            continue;
        }
        if (s != t) {        // If not tail, try to unsplice
            QNode sn = s.next;
            if (sn == s || pred.casNext(s, sn))
                return;
        }
        QNode dp = cleanMe;
        if (dp != null) {    // Try unlinking previous cancelled node
            QNode d = dp.next;
            QNode dn;
            if (d == null ||               // d is gone or
                d == dp ||                 // d is off list or
                !d.isCancelled() ||        // d not cancelled or
                (d != t &&                 // d not tail and
                    (dn = d.next) != null &&  //   has successor
                    dn != d &&                //   that is on list
                    dp.casNext(d, dn)))       // d unspliced
                casCleanMe(dp, null);
            if (dp == pred)
                return;      // s is already saved node
        } else if (casCleanMe(null, pred))
            return;          // Postpone cleaning s
    }
}

我们看方法中的注释:

不论任何情况,列表上最后插入的节点不能被删除。为了适应这一点,如果我们不能删除s,我们将其前驱设置为“CurrMe”,先删除以前保存的版本。节点s或先前保存的节点中的至少一个总是可以被删除。

该方法的主要逻辑如下:

1、删除的节点不是queue尾节点, 这时直接以pred.casNext(s, s.next)方式来进行删除

2、删除的节点是队尾节点:

  • 此时 cleanMe == null, 则 前继节点pred标记为 cleanMe, 为下次删除做准备
  • 此时cleanMe != null,先删除上次需要删除的节点,然后将cleanMe置为null,让后再将pred赋值给cleanMe

非公平模式

TransferStack实现的transfer方法如下:

E transfer(E e, boolean timed, long nanos) {
    SNode s = null; // constructed/reused as needed
    // 获取当前节点的模式
    int mode = (e == null) ? REQUEST : DATA;

    for (;;) {
        SNode h = head;
        // 栈为空或者当前节点模式与头节点模式一样,将节点压入栈内,等待匹配
        if (h == null || h.mode == mode) {  // empty or same-mode
            // 超时
            if (timed && nanos <= 0) {      // can't wait
                // 节点被取消了,弹出被取消的节点
                if (h != null && h.isCancelled())
                    casHead(h, h.next);     // pop cancelled node
                else
                    return null;
            } 
            // 未超时,创建SNode节点
            else if (casHead(h, s = snode(s, e, h, mode))) {
                // 自旋,等待匹配
                SNode m = awaitFulfill(s, timed, nanos);
                // 返回的m == s 表示该节点被取消了或者超时、中断了
                if (m == s) {               // wait was cancelled
                    // 清理节点s,返回null
                    clean(s);
                    return null;
                }

                // 因为通过前面一步将s替换成了head,如果h.next == s,则表示有其他节点插入到s前面了,变成了head
                // 且该节点就是与节点s匹配的节点
                if ((h = head) != null && h.next == s)
                    casHead(h, s.next);     // help s's fulfiller
                // 如果是请求则返回匹配的域,否则返回节点s的域
                return (E) ((mode == REQUEST) ? m.item : s.item);
            }
        } 
        // 如果栈不为null,且两者模式不匹配(h != null && h.mode != mode)
        // 说明他们是一队对等匹配的节点,尝试用当前节点s来满足h节点
        else if (!isFulfilling(h.mode)) { // try to fulfill
            // head 节点已经取消了,向前推进
            if (h.isCancelled())            // already cancelled
                casHead(h, h.next);         // pop and retry
            // 尝试将当前节点打上“正在匹配”的标记,并设置为head
            else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
                for (;;) { // loop until matched or waiters disappear
                    // s为当前节点,m是s的next节点,
                    // m节点是s节点的匹配节点
                    SNode m = s.next;       // m is s's match
                    // m == null,其他节点把m节点匹配走了
                    if (m == null) {        // all waiters are gone
                        // 将s弹出
                        casHead(s, null);   // pop fulfill node
                        // 将s置空,下轮循环的时候还会新建
                        s = null;           // use new node next time
                        break;              // restart main loop
                    }
                    // 获取m的next节点
                    SNode mn = m.next;
                    // 尝试匹配
                    if (m.tryMatch(s)) {
                        // 匹配成功,将s、m弹出
                        casHead(s, mn);     // pop both s and m
                        return (E) ((mode == REQUEST) ? m.item : s.item);
                    } else                  // lost match
                        // 如果没有匹配成功,说明有其他线程已经匹配了,把m移出
                        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、如果当前的交易栈是空的,或者包含与请求交易节点模式相同的节点,那么就将这个请求交易的节点作为新的栈顶节点,等待被下一个请求交易的节点匹配,最后会返回匹配节点的数据或者null,如果被取消则会返回null。
2、如果当前交易栈不为空,并且请求交易的节点和当前栈顶节点模式互补,那么将这个请求交易的节点的模式变为FULFILLING,然后将其压入栈中,和互补的节点进行匹配,完成交易之后将两个节点一起弹出,并且返回交易的数据。

3、如果栈顶已经存在一个模式为FULFILLING的节点,说明栈顶的节点正在进行匹配,那么就帮助这个栈顶节点快速完成交易,然后继续交易。

当节点加入栈内后,通过调用awaitFulfill()方法自旋等待节点匹配:

SNode awaitFulfill(SNode s, boolean timed, long nanos) {
    // 计算超时时间点
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    // 获取当前线程对象
    Thread w = Thread.currentThread();

    // 自旋次数
    // shouldSpin 用于检测当前节点是否需要自旋
    // 如果栈为空、该节点是首节点或者该节点是匹配节点,则先采用自旋,否则阻塞
    int spins = (shouldSpin(s) ?
            (timed ? maxTimedSpins : maxUntimedSpins) : 0);
    for (;;) {
        // 线程中断了,取消该节点
        if (w.isInterrupted())
            s.tryCancel();

        // 匹配节点
        SNode m = s.match;

        // 如果匹配节点m不为空,则表示匹配成功,直接返回
        if (m != null)
            return m;
        // 超时
        if (timed) {
            nanos = deadline - System.nanoTime();
            // 节点超时,取消
            if (nanos <= 0L) {
                s.tryCancel();
                continue;
            }
        }

        // 每次自旋的时候都需要检查自身是否满足自旋条件,满足就 - 1,否则为0
        if (spins > 0)
            spins = shouldSpin(s) ? (spins-1) : 0;

        // 第一次阻塞时,会将当前线程设置到s上
        else if (s.waiter == null)
            s.waiter = w;

        // 阻塞当前线程
        else if (!timed)
            LockSupport.park(this);
        // 超时
        else if (nanos > spinForTimeoutThreshold)
            LockSupport.parkNanos(this, nanos);
    }
}

这个线程一直阻塞直到被匹配,在阻塞之前首先会自旋,这个自旋会在阻塞之前进行,它会调用shouldSpin方法来进行判断是否需要自旋,下面展示了shouldSpin这个方法:

boolean shouldSpin(SNode s) {
    SNode h = head;
    return (h == s || h == null || isFulfilling(h.mode));
}

如果当前节点在栈顶,并且正在请求交易,那么就应该自旋。在多CPU的环境下,这种情况下的自旋是有必要的,因为很可能立刻就会有新的线程到来,那么就会立刻进行交易而不需要进行阻塞,然后被唤醒,这是需要过程的,所以这样的自旋等待是值得的。

若线程被中断了,则调用tryCancel()方法取消该节点:

void tryCancel() {
    UNSAFE.compareAndSwapObject(this, matchOffset, null, this);
}

该过程和TransferQueue相同。awaitFullfill()方法如果返回的m == s,则表示当前节点已经中断取消了,则需要调用clean()方法,清理节点s:

void clean(SNode s) {
    s.item = null;   // forget item
    s.waiter = null; // forget thread

    SNode past = s.next;
    if (past != null && past.isCancelled())
        past = past.next;

    // Absorb cancelled nodes at head
    SNode p;
    while ((p = head) != null && p != past && p.isCancelled())
        casHead(p, p.next);

    // Unsplice embedded nodes
    while (p != null && p != past) {
        SNode n = p.next;
        if (n != null && n.isCancelled())
            p.casNext(n, n.next);
        else
            p = n;
    }
}

clean()方法就是将head节点到s节点之间所有已经取消的节点全部移出。

SynchronousQueue的实现有点难,看不懂啊。。。

参考资料

【死磕Java并发】-----J.U.C之阻塞队列:SynchronousQueue

Java阻塞队列SynchronousQueue详解

猜你喜欢

转载自blog.csdn.net/qq_38293564/article/details/80604194