阻塞队列之SynchronousQueue

SynchronousQueue是一个特殊的阻塞队列,生产者插入操作后只能等待消费者移除操作,它没有任何容量,甚至没有存储一个元素的容量。在SynchronousQueue中不能使用peek方法,因为元素只有被移除时才存在,只有消费者移除了元素生产者才能往队列中插入元素,当然更不能进行迭代。队列的头节点是第一个排队插入元素的线程,队列是不允许存储null的元素。生产者和消费者必须互相等待,这样经过一个元素的生产到消费的过程,然后一致离开。

SynchronousQueue有两种策略,一种是公平模式,使用队列来完成,一种是非公平模式,使用栈来完成,不论是栈或队列都是用链表来实现的 。看一下SynchronousQueue的创建:

SynchronousQueue<String> s = new SynchronousQueue<>();
SynchronousQueue<String> b = new SynchronousQueue<>(true);

接下来看一下它的构造方法

    /**
     * 默认无参构造
     */
    public SynchronousQueue() {
        this(false);
    }

    /**
     * 指定是否使用公平模式
     */
    public SynchronousQueue(boolean fair) {
        transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
    }

默认是创建一个非公平模式,也可以实现公平模式,如果是公平模式的话,可以保证第一个队首的线程是等待时间最长的线程,这时可以把SynchronousQueue看作是一个FIFO队列。无论是队列或栈,它们都继承了Transferer类。

基本属性

   /** CPU的数量 */
    static final int NCPUS = Runtime.getRuntime().availableProcessors();

    static final int maxTimedSpins = (NCPUS < 2) ? 0 : 32;

    static final int maxUntimedSpins = maxTimedSpins * 16;

    static final long spinForTimeoutThreshold = 1000L;

这四个属性是用来设置自旋时间的。阻塞是一个非常消耗性能的操作,要进行线程之间上下文的切换,所以一般在阻塞之前进行自旋,不停的使用死循环进行检测,当然不能永远停在循环中,必须要设定时间限定,如果在时间限定内通过自旋完成了某种操作,那就不用阻塞提高了响应速度。如果超时要进行阻塞。

当然在竞争激烈的情况下,自旋的时间长点也是可以的,但是过长的自旋也会白白浪费CPU的时间,所以时间限定并没有一个准确的值,要根据不同的情况进行设定。

首先是获取CPU的数量,分两种情况,一种是设定了时间限的自旋,如果CPU的数量是1,那就不进行自旋,只有一个CPU在进行自旋就不能进行其他操作了,CPU的数量>=2,设定maxTimedSpins为32。另一种是没有设定时间限的自旋,如果CPU的数量>=2,把maxUntimeSpins设为32*16,如果CPU的数量为1,把maxUntimeSpins设为0。

spinForTimeoutThreshold是为了防止自定义的时间过长而设置的,单位是纳秒,如果设定的时间>这个值,那就把spinForTimeoutThreshold当作时间限。

 private transient volatile Transferer<E> transferer;
SynchronousQueue内有两个内部类TransferQueue和TransferStack,它们都继承了Transfer类,transferer就是具体实现的引用,所有方法都要基于transferer去执行。


TransferStack

        /* Modes for SNodes, ORed together in node fields */
        /** 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;
TransferStack的三种状态,REQUEST表示消费者,DATA表示生产者,FULFILLING表示匹配另一个生产者或者消费者。任何线程对TransferStack的操作都应该是这三种状态中的一种。

看一下它的内部类snode。

       /** Node class for TransferStacks. */
        static final class SNode {
            volatile SNode next;        // 栈中的下一个节点
            volatile SNode match;       // 相匹配的节点
            volatile Thread waiter;     // 当前节点代表的线程
            Object item;                // 具体的item值或者null
            int mode;                   // 代表当前线程的模式
            // item和mode字段不需要设置成volatile类型的,因为它们总是在写入之前,读操作之后

            SNode(Object item) {  // snode的构造方法
                this.item = item;
            }
        }
         // 创建一个snode节点
        static SNode snode(SNode s, Object e, SNode next, int mode) {
            if (s == null) s = new SNode(e);
            s.mode = mode;
            s.next = next;
            return s;
        }

继续看一下snode类的方法。

casNext就是设置当前节点的下一个节点。

tryCancel就是取消当前操作。

isCancelled就是把当前的match匹配变成this,如果当前节点的match是空说明当前节点的任务还没有完成,换成this匹配自己说明任务被取消。

重点是tryMatch方法。

            boolean tryMatch(SNode s) {
                if (match == null &&
                    UNSAFE.compareAndSwapObject(this, matchOffset, null, s)) {
                    Thread w = waiter;
                    if (w != null) {    // waiters need at most one unpark
                        waiter = null;
                        LockSupport.unpark(w);
                    }
                    return true;
                }
                return match == s;
            }

当前节点与s节点进行匹配,如果当前节点的match为null && 利用cas把match设为s,获取当前节点的等待线程,如果等待线程不为null,将当前节点的等待线程重设为null,释放当前的等待线程,直接返回true,表示匹配成功。如果当前节点的match不为null  || 利用cas设置失败,判断match==s,如果相等,表示匹配成功,直接返回true,如果不相等,直接返回false。

transfer方法

        /**
         * 放入或取出item
         */
        @SuppressWarnings("unchecked")
        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) {  // 栈为null 或 栈顶节点的模式与当前元素模式相同
                    if (timed && nanos <= 0) {      // 设置了timed而且等待时间<0,不进行等待
                        if (h != null && h.isCancelled())   // 栈顶节点不为null && 被取消
                            casHead(h, h.next);     // 利用cas设置头节点的下一个节点变成头节点,就是弹出取消的节点
                        else                       // 栈为null 或 栈顶节点没有被取消,直接返回null
                            return null;
                    } else if (casHead(h, s = snode(s, e, h, mode))) {  // 创建一个snode节点,把该节点设置为头节点
                        SNode m = awaitFulfill(s, timed, nanos);   // 自旋阻塞一直匹配到节点 
                        if (m == s) {               // 匹配的节点是s,说明节点被取消了,清除s,直接返回null
                            clean(s);               
                            return null;            
                        }
                        if ((h = head) != null && h.next == s)  // 新的头节点不为null && 新头节点的next为s,说明有节点插入到s之前
                            casHead(h, s.next);     // 移除h和s节点,设置s的next为头节点
                        return (E) ((mode == REQUEST) ? m.item : s.item);  // 根据mode返回元素
                    }
                } else if (!isFulfilling(h.mode)) { // 如果当前栈顶节点的模式不是Fulfilling,尝试去匹配
                    if (h.isCancelled())            // 栈顶节点被取消直接设置新的头节点然后进行重试
                        casHead(h, h.next);       
                    else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {   // 新建一个snode节点把该节点设为头节点
                        for (;;) {    // 自旋直到匹配成功或等待的线程消失
                            SNode m = s.next;       // 保存s的next节点为m,m应该是s的匹配节点
                            if (m == null) {        // 如果m为null,说明其他节点把m匹配了,把头节点设为null
                                casHead(s, null);   
                                s = null;           // 把s置为null以便下次循环时使用一个新的节点
                                break;              // 重新开始主循环
                            }
                            SNode mn = m.next;      // 保存m的next节点为mn
                            if (m.tryMatch(s)) {    // 如果m和s匹配成功
                                casHead(s, mn);     // 弹出m和s,设置mn为头节点
                                return (E) ((mode == REQUEST) ? m.item : s.item);  // 根据mode返回元素
                            } else                  // 如果匹配失败
                                s.casNext(m, mn);   // 弹出m节点
                        }
                    }
                } else {                            
                    SNode m = h.next;               // 保存头节点的next节点m
                    if (m == null)                  // m为null表示已经被其他节点匹配了,需要弹出头节点
                        casHead(h, null);          
                    else {                          // m不为null
                        SNode mn = m.next;          // 保存m的next节点mn
                        if (m.tryMatch(h))          // 如果m和h匹配成功
                            casHead(h, mn);         // 弹出m和h,设置mn为头节点
                        else                        // 如果m和h匹配失败
                            h.casNext(m, mn);       // 弹出m节点
                    }
                }
            }
        }

这个方法用于生产或消费一个元素,主要分三个步骤。

1. 如果当前栈为空或者栈顶元素的模式与当前元素模式一样:① 判断开启了时间限定模式并且限定的时间的时间已经<=0,如果栈顶节点不为null && 栈顶节点被取消,直接弹出栈顶节点,设置下一个节点为栈顶节点,如果栈为null或到了时间限定后栈顶节点仍然没有被取消,直接返回null。

② 尝试将当前节点设为栈顶节点,通过自旋等待匹配的节点m,如果m=当前节点本身,说明当前节点被取消了,清除节点直接返回null。如果m != 当前节点,说明匹配成功,再次判断一下当前栈顶节点发生了改变,取消当前节点和栈顶节点, 最后返回相匹配的节点。

2. 如果当前栈不为空并且当前节点与栈顶节点的模式相匹配的话:① 如果栈顶节点被取消直接设置下一个节点为栈顶节点。

② 把当前节点给上FULFILLING标记,尝试把当前节点作为头节点,循环直到匹配到相应的节点或等待的节点消失,如果相匹配的节点被其他节点匹配了,要重新寻找头节点继续进行。匹配成功后将这两个节点出栈,返回匹配节点的元素。

3. 如果有节点正在匹配,帮助这个节点完成匹配。如果这个节点的匹配节点为null,说明已经被其它节点匹配过了,直接弹出头节点。如果不为null,匹配成功,弹出这两个节点。继续执行主循环。

awaitFulfill

        SNode awaitFulfill(SNode s, boolean timed, long nanos) {
           
             // 如果开启了时间限定模式得到时间的终止点
            final long deadline = timed ? System.nanoTime() + nanos : 0L;
            Thread w = Thread.currentThread();   // 获取当前线程
            int spins = (shouldSpin(s) ?    // 设置自旋次数
                         (timed ? maxTimedSpins : maxUntimedSpins) : 0);
            for (;;) {
                if (w.isInterrupted())  // 当前线程被中断尝试着取消当前节点
                    s.tryCancel();
                SNode m = s.match;   // s的匹配节点m
                if (m != null)   // m不为null,说明存在匹配节点直接返回
                    return m;
                if (timed) {      // 开启了时间限定
                    nanos = deadline - System.nanoTime();    // 计算剩余等待的时间
                    if (nanos <= 0L) {              // nanos<=0,  超时,取消当前节点继续循环
                        s.tryCancel();
                        continue;
                    }
                }
                if (spins > 0)     // 自旋次数>0,每次循环都要自减1
                    spins = shouldSpin(s) ? (spins-1) : 0;
                else if (s.waiter == null)   // s的等待线程为null,直接设置当前线程为waiter
                    s.waiter = w; // establish waiter so can park next iter
                else if (!timed)    // 没有开启时间限定模式,直接挂起当前线程
                    LockSupport.park(this);
                else if (nanos > spinForTimeoutThreshold)    // 如果nanos>1000l,挂起当前线程,否则不挂起线程
                    LockSupport.parkNanos(this, nanos);
            }
        }

awaitFulfill会自旋阻塞一直匹配到节点,调用shouldSpin方法来判定是否要进行自旋。

        /**
         * 如果当前节点是头节点 或者 栈为null 或者 当前节点与栈顶节点模式不匹配
         */
        boolean shouldSpin(SNode s) {
            SNode h = head;
            return (h == s || h == null || isFulfilling(h.mode));
        }

在这种条件下,不需要阻塞了,生产者消费者马上到来,这种优化是非常有必要的。在自旋中一直要检测当前线程是否被中断,如果被中断要取消当前线程,取消当前线程就是把当前的引用指向自己,所以返回的匹配节点肯定是自己本身m==s,所以就要调用clean方法清除该节点。

        void clean(SNode s) {
            s.item = null;   // 当前节点的item设为null
            s.waiter = null; // 当前节点的waiter设为null

            SNode past = s.next;   // s的next节点past
            if (past != null && past.isCancelled())  // past不为null && past被取消,重新设置past节点
                past = past.next;

            SNode p;
            while ((p = head) != null && p != past && p.isCancelled())// 从栈顶节点开始遍历一直到past节点,将这中间连续被取消的节点移除
                casHead(p, p.next);      

            // 移除上面步骤中没有被移除的非连续的被取消的节点
            while (p != null && p != past) {
                SNode n = p.next;
                if (n != null && n.isCancelled())
                    p.casNext(n, n.next);
                else
                    p = n;
            }
        }

可以看出清理被取消的节点,而且还顺带遍历栈,把栈中从栈顶节点到该节点(不包括该节点)被取消的节点全部移除。为什么要用两个while循环那?很是纳闷。

TransferQueue

看一下Qnode类。

        static final class QNode {
            volatile QNode next;          // 队列中的下一个节点
            volatile Object item;         // 元素值
            volatile Thread waiter;       // 当前节点线程
            final boolean isData;         // 当前是否为生产者
            // qnode的构造方法
            QNode(Object item, boolean isData) {
                this.item = item;
                this.isData = isData;
            }
        }

transferqueue的基本属性。

        /** 队列的头节点 */
        transient volatile QNode head;
        /** 队列的尾节点 */
        transient volatile QNode tail;
        /**
         * 一个被取消的节点或许还没有被移除队列,因为当它被取消的时候可能是最后一个被插入的节点
         */
        transient volatile QNode cleanMe;
transferqueue的构造方法,初始化一个空的节点,并且头节点和尾节点都指向它。
        TransferQueue() {
            QNode h = new QNode(null, false); // initialize to dummy node.
            head = h;
            tail = h;
        }

transfer方法

        /**
         * 插入或取出元素
         */
        @SuppressWarnings("unchecked")
        E transfer(E e, boolean timed, long nanos) {
            
            QNode s = null; // constructed/reused as needed
            boolean isData = (e != null);  // e==null,说明是取出元素;e!=null,说明是插入元素,即生产者请求将数据插入队列

            for (;;) {
                QNode t = tail;
                QNode h = head;
                if (t == null || h == null)    // 未初始化的头尾节点
                    continue;           // 继续自旋

                if (h == t || t.isData == isData) { // 队列为null 或 尾节点的模式与当前节点的模式相同
                    QNode tn = t.next;   //  保存尾节点的next节点为tn
                    if (t != tail)   // t不是尾节点,重试
                        continue;            
                    if (tn != null) {   // tn不为null,说明有其它线程已经抢占添加了tn节点
                        advanceTail(t, tn);  //  如果t是尾节点利用cas把tn设为尾节点
                        continue;    // 继续重试
                    }
                    if (timed && nanos <= 0)   // 如果设定了时间限定并且时间已到,不在进行等待,直接返回null
                        return null;
                    if (s == null)    // 新建一个qnode节点,赋给s
                        s = new QNode(e, isData);
                    if (!t.casNext(null, s))   // 如果不能使用cas把t的next设为s,重试
                        continue;

                    advanceTail(t, s);    // 重设尾节点
                    Object x = awaitFulfill(s, e, timed, nanos);  // 自旋阻塞直到匹配到s节点
                    if (x == s) {      // s的匹配节点是自己本身,说明节点被取消,清除s,直接返回null
                        clean(t, s);
                        return null;
                    }

                    if (!s.isOffList()) {           // s节点还没有离开队列
                        advanceHead(t, s);          // 将s设置为头节点
                        if (x != null)              
                            s.item = s;
                        s.waiter = null;           // 方法s节点的线程
                    }
                    return (x != null) ? (E)x : e;

                } else {                            // 互补模式
                    QNode m = h.next;             
                    if (t != tail || m == null || h != head)   // t不是尾节点 或 m为null 或 h不是头节点,读不一致,重试
                        continue;               

                    Object x = m.item;     
                    if (isData == (x != null) ||    // 判定x的模式是否与isdata相同,相同说明m早已经被匹配过了
                        x == m ||                   // m被取消
                        !m.casItem(x, e)) {         // cas失败
                        advanceHead(h, m);          // 将m设为头节点出列重试
                        continue;
                    }

                    advanceHead(h, m);              // 匹配成功将m设为头节点
                    LockSupport.unpark(m.waiter);   // 释放m的等待线程
                    return (x != null) ? (E)x : e;
                }
            }
        }
transfer方法分三种情况,步骤如下:
1.  如果队列没有初始化,自旋重试。
2.  队列为空或当前节点的模式与尾节点模式相同,将节点加入到等待队列中,直到匹配的节点被取消或者超时,匹配成功,如果返回的节点本身说明node被取消或超时,直接返回null,否则返回真正的值。
3.  队列不为空或当前节点是尾节点匹配的节点,返回匹配节点的元素。

4.  如果有节点正在匹配,帮助这个节点完成匹配。

如果队列为空,节点入队,调用awaitFulfill自旋,直到获取到匹配的节点或节点被取消超时。

        Object awaitFulfill(QNode s, E e, boolean timed, long nanos) {
            /* 如果设定了时间限定计算超时时间 */
            final long deadline = timed ? System.nanoTime() + nanos : 0L;
            Thread w = Thread.currentThread();   // 获取当前线程
            int spins = ((head.next == s) ?   // 自旋次数,如果这个节点的前节点正好是head节点,那就进行自旋操作,主要是避免减少线程之间的上下文切换
                         (timed ? maxTimedSpins : maxUntimedSpins) : 0);
            for (;;) {
                if (w.isInterrupted())  // 线程被中断过直接移除该节点
                    s.tryCancel(e);
                Object x = s.item;
                if (x != e)  // 线程被中断 或 进行了阻塞唤醒 或 超时,那么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为null设置当前线程为它的waiter
                    s.waiter = w;
                else if (!timed)       // 没有开启时间限定直接挂起当前线程进行阻塞
                    LockSupport.park(this);
                else if (nanos > spinForTimeoutThreshold)   // 设定的时间>1000l,在指定时间内挂起当前线程
                    LockSupport.parkNanos(this, nanos);
            }
        }

head节点永远是个空节点,所以先判断当前节点是否的head的next节点,这样就先进行自旋,主要还是为了生产者或消费者来了就能立刻匹配,如果挂起再进行唤醒太消耗性能。再自旋中达到一定次数就挂起线程,不能无限自旋,以免浪费cpu的时间。

如果返回的节点是自己本身,所以x==s,就要调用clean方法取消该节点。

        void clean(QNode pred, QNode s) {
            s.waiter = null; // 把s节点的线程设为null
         
            while (pred.next == s) { // Return early if already unlinked
                QNode h = head;
                QNode hn = h.next;   
                if (hn != null && hn.isCancelled()) {  // hn不为null && hn被取消,重新头节点为hn,继续重试
                    advanceHead(h, hn);
                    continue;
                }
                QNode t = tail;  // 获取尾节点,确保对尾节点读的一致性
                if (t == h)   // 队列为空直接返回
                    return;
                QNode tn = t.next;  // 保存尾节点的next节点
                if (t != tail)  // 
                    continue;
                if (tn != null) {
                    advanceTail(t, tn);
                    continue;
                }
                if (s != t) {        // s不是尾节点,如果s被取消 或 断开s直接退出
                    QNode sn = s.next;
                    if (sn == s || pred.casNext(s, sn))
                        return;
                }
                QNode dp = cleanMe;  // 这种情况出现删除的只能是队尾节点
                if (dp != null) {    // 如果dp不为null,说明了
                    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)   // 如果dp==pred,说明清除s成功,直接返回
                        return;      // s is already saved node
                } else if (casCleanMe(null, pred))  // cleanMe是null,所以使用cas把pred设为cleanMe,目的就是为了清除s做准备
                    return;         
            }
        }

这个clean方法没有完全读懂,感觉好有难度。引用一下别人的分析。

 1. 删除的节点不是queue尾节点, 这时直接 pred.casNext(s, s.next) 方式来进行删除。
 2. 删除的节点是队尾节点
    此时 cleanMe ==null , 则 前继节点pred标记为 cleanMe, 为下次删除做准备 
    此时 cleanMe != null, 先删除上次需要删除的节点, 然后将 cleanMe至null, 让后再将 pred 赋值给 cleanMe


SynchronousQueue代码真复杂啊,同时用到了栈和队列,底层都是使用链表完成的,可见链表对我们而言多么的重要,需要我们对它有清醒的认识,虽然代码比较复杂,但是实际就分那几种情况,我是真的看着代码,然后自己大脑中想象出一个这样的并发场景,一步步的往下执行,也可以画图使自己有个更清晰的理解。有了这个基础,我们再看线程池的框架分析就有了更好的理解。源码的分析真的很费脑啊!尤其Doug lea大神的并发包!!!







猜你喜欢

转载自blog.csdn.net/qq_30572275/article/details/80457363
今日推荐