Java并发学习(二十七)-LinkedTransferQueue分析

前后花了一个多礼拜来学习LinkedTransferQueue,整理了一些新的和了解,就此记下来。

What is LinkedTransferQueue

先来看看LinkedTransferQueue是什么?
它是Java7才出现的一个新的阻塞队列,继承了AbstractQueue 抽象类,实现了Java7出现的TransferQueue的接口。
其他的阻塞队列,用生产者消费者来模拟的话,生产者生产数据,如果队列没有满,放下数据就走;而消费者获取数据时候,就是看到有数据的话,也是获取数据就走。
这个队列有点像SynchronousQueue,但是比SynchronousQueue功能上要强大,SynchronousQueue是被设计为没有容量的,生产者放数据时候,如果没有消费者获取,则需要阻塞等待直到一个消费者过来获取,详情可参阅:Java并发学习(二十六)-Java中SynchronousQueue分析 。
而对于LinkedTransferQueue,也是具有阻塞性的读取,但是里面是有容量的。假设t1去里面生产物品,此时没有消费者,则他需要等待直到有人来取,而此时,如果有t2来生产则,也需要等待直到有人取走。而如果t1、t2分别为消费者,没有数据时候,它们也需要进入队列等待,直到有生产者跟他们交换完数据才行。

接下来看看它的定义:

LinkedTransferQueue结构

public class LinkedTransferQueue<E> extends AbstractQueue<E>
    implements TransferQueue<E>, java.io.Serializable {
    //处理器数量
    private static final boolean MP =
        Runtime.getRuntime().availableProcessors() > 1;

    // 自旋次数。前驱节点正在处理,当前节点需要自旋的次数。一定要是2的倍数。
    private static final int FRONT_SPINS   = 1 << 7;

    //自旋的次数。一定要为2的倍数。
    private static final int CHAINED_SPINS = FRONT_SPINS >>> 1;

    //清扫cancell的node,
    static final int SWEEP_THRESHOLD = 32;

    //头节点
    transient volatile Node head;

    //尾节点。
    private transient volatile Node tail;

    //看起来失败的次数,然后尝试去清除删除的节点。
    private transient volatile int sweepVotes;
    //供xfer使用,表明xfer要干什么
    private static final int NOW   = 0; // for untimed poll, tryTransfer
    private static final int ASYNC = 1; // for offer, put, add
    private static final int SYNC  = 2; // for transfer, take
    private static final int TIMED = 3; // for timed poll, tryTransfer

接下来看看TransferQueue接口:

public interface TransferQueue extends BlockingQueue {
    /**
     * 不是阻塞操作,如果能够和相符合线程转化,则转化并返回true,
     * 若没有相符合线程,则直接返回false,不进行等待。
     */
    boolean tryTransfer(E e);

    /**
     * 和前一个相比,如果没有相符合的线程交换,则需要阻塞等待。
     */
    void transfer(E e) throws InterruptedException;

    /**
     * 阻塞性的,有超时时间,超过时间没有交换成功,则返回false。
     */
    boolean tryTransfer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;

    /**
     * 如果有等待交换的线程,则返回true。
     */
    boolean hasWaitingConsumer();

    /**
     * 获取所有等待获取元素的消费线程数量,为take或者poll方法。
     */
    int getWaitingConsumerCount();
}

再看看它的存储节点定义:

    static final class Node {
        final boolean isData;   // false if this is a request node  //是否为数据。
        volatile Object item;   // initially non-null if isData; CASed to match  match的node。
        volatile Node next;           //下一个node。
        volatile Thread waiter; // null until waiting           //等待的线程。

        // CAS methods for fields
        final boolean casNext(Node cmp, Node val) {
            return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
        }

        final boolean casItem(Object cmp, Object val) {
            // assert cmp == null || cmp.getClass() != Node.class;
            return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
        }

        /**
         * Constructs a new node.  Uses relaxed write because item can
         * only be seen after publication via casNext.
         */
        Node(Object item, boolean isData) {
            UNSAFE.putObject(this, itemOffset, item); // relaxed write
            this.isData = isData;
        }

        //忘记下一个,也就是让自己指向自己。
        final void forgetNext() {
            UNSAFE.putObject(this, nextOffset, this);
        }

        //忘记item和waiter。
        final void forgetContents() {
            UNSAFE.putObject(this, itemOffset, this);
            UNSAFE.putObject(this, waiterOffset, null);
        }

        //判断是否match。
        final boolean isMatched() {
            Object x = item;
            return (x == this) || ((x == null) == isData);
        }

        //是否为不匹配的request。
        final boolean isUnmatchedRequest() {
            return !isData && item == null;
        }

        //检验能不能配对。
        final boolean cannotPrecede(boolean haveData) {
            boolean d = isData;
            Object x;
            return d != haveData && (x = item) != this && (x != null) == d;
        }

        /**
         * Tries to artificially match a data node -- used by remove.
         * 
         * 来match数据。
         * 
         * 当前item不为null
         * 不为本身。
         */
        final boolean tryMatchData() {
            // assert isData;
            Object x = item;
            if (x != null && x != this && casItem(x, null)) {
                LockSupport.unpark(waiter);
                return true;
            }
            return false;
        }

        private static final long serialVersionUID = -3375979862319811754L;

        // Unsafe mechanics
        private static final sun.misc.Unsafe UNSAFE;
        private static final long itemOffset;
        private static final long nextOffset;
        private static final long waiterOffset;
        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class k = Node.class;
                itemOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("item"));
                nextOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("next"));
                waiterOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("waiter"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }

LinkedTransferQueue主要方法

在LinkedTransferQueue里面,方法都是调用xfer方法,只是传入的值不同。
提供元素:

public void put(E e) {  
    xfer(e, true, ASYNC, 0);  
}  

public boolean offer(E e, long timeout, TimeUnit unit) {  
    xfer(e, true, ASYNC, 0);  
    return true;  
}  

public boolean offer(E e) {  
    xfer(e, true, ASYNC, 0);  
    return true;  
}  

public boolean add(E e) {  
    xfer(e, true, ASYNC, 0);  
    return true;  
}  

public boolean tryTransfer(E e) {  
    return xfer(e, true, NOW, 0) == null;  
}  

public void transfer(E e) throws InterruptedException {  
    if (xfer(e, true, SYNC, 0) != null) {  
        Thread.interrupted(); // failure possible only due to interrupt  
        throw new InterruptedException();  
    }  
}  

public boolean tryTransfer(E e, long timeout, TimeUnit unit)  
    throws InterruptedException {  
    if (xfer(e, true, TIMED, unit.toNanos(timeout)) == null)  
        return true;  
    if (!Thread.interrupted())  
        return false;  
    throw new InterruptedException();  
}  

获取元素:

public E take() throws InterruptedException {  
    E e = xfer(null, false, SYNC, 0);  
    if (e != null)  
        return e;  
    Thread.interrupted();  
    throw new InterruptedException();  
}  

public E poll(long timeout, TimeUnit unit) throws InterruptedException {  
    E e = xfer(null, false, TIMED, unit.toNanos(timeout));  
    if (e != null || !Thread.interrupted())  
        return e;  
    throw new InterruptedException();  
}  

public E poll() {  
    return xfer(null, false, NOW, 0);  
}  

下面来看锁调用到的xfer方法。

xfer

由上面代码所知,xfer方法有接受4个不同的how操作:

  • NOW = 0; //用于没有超时的poll和tryTransfer方法
  • ASYNC = 1; //用于offer,put,add
  • SYNC = 2; //用于transfer,take方法
  • TIMED = 3; //用于有超时的poll,tryTransfer方法

下面来看xfer方法:

    private E xfer(E e, boolean haveData, int how, long nanos) {
        if (haveData && (e == null))      //如果有data,但是e又为null。不正常,则报错。
            throw new NullPointerException();
        Node s = null;                        // 如果需要的话,代表被加入队列的节点

        retry:
        for (;;) {                            // restart on append race  自旋。
            for (Node h = head, p = h; p != null;) { // find & match first node     //从头开始找。
                boolean isData = p.isData;                //看p的数据类型。
                Object item = p.item;                    //获取p的数据
                if (item != p && (item != null) == isData) { // item!=p:没有匹配过,item有值
                    if (isData == haveData)   // can't match   //不能匹配。
                        break;
                    if (p.casItem(item, e)) { // match 以下可以匹配  把p的item设为e。
                        for (Node q = p; q != h;) {     //从p节点开始,
                            Node n = q.next;  // update by 2 unless singleton
                            if (head == h && casHead(h, n == null ? q : n)) {    //如果head是头节点h,那么尝试用n或者q替换。
                                h.forgetNext();    //并且让去除h。
                                break;
                            }                 // advance and retry
                            if ((h = head)   == null ||                  //head不为null。q也不为null。q还没有被matched,就退出循环。
                                (q = h.next) == null || !q.isMatched())
                                break;        // unless slack < 2
                        }
                        LockSupport.unpark(p.waiter);     //唤醒p。因为和p匹配完了
                        return LinkedTransferQueue.cast(item);
                    }
                }
                Node n = p.next;        //n为p的下一个节点。
                p = (p != n) ? n : (h = head); // Use head if p offlist    //如果p到尾端了,那么从h开始。
            }

            // 如果没有找到匹配的节点,则进行处理
            // NOW为untimed poll, tryTransfer,不需要入队
            if (how != NOW) {                 //符合这个判断需要入队              
                if (s == null)
                    s = new Node(e, haveData);   //初始化s。
                Node pred = tryAppend(s, haveData);         //把s加入到当前节点后面。
                if (pred == null)
                    continue retry;           // lost race vs opposite mode   重新自旋。
                // ASYNC不需要阻塞等待
                if (how != ASYNC)
                    return awaitMatch(s, pred, e, (how == TIMED), nanos);    //阻塞性的match。
            }
            return e; // not waiting
        }
    }

整个xfer的思想就是,找到了可以交换的节点就进行匹配,否则就入队(NOW是直接返回false的)。

下面看看它的tryAppend方法:

    /**尽力去把s加到队尾。
     * 
     * 返回null,说明被其他线程抢先而失败。
     */
    private Node tryAppend(Node s, boolean haveData) {
        for (Node t = tail, p = t;;) {        // move p to last node and append    p为尾节点。
            Node n, u;                        // temps for reads of next & tail
            if (p == null && (p = head) == null) {          //当队列中没有任何节点时候。
                if (casHead(null, s))
                    return s;                 // initialize
            }
            else if (p.cannotPrecede(haveData))
                return null;                  // lost race vs opposite mode    竞争失败。
            else if ((n = p.next) != null)    // not last; keep traversing    //不是最后一个
                p = p != t && t != (u = tail) ? (t = u) : // stale tail
                    (p != n) ? n : null;      // restart if off list     最终,p要么往后移动一个,要么为null也就是从头来。
            else if (!p.casNext(null, s))              //如果CAS失败,那么往后移动一个重来一次。
                p = p.next;                   // re-read on CAS failure
            else {
                if (p != t) {                 // update if slack now >= 2
                    while ((tail != t || !casTail(t, s)) &&               //t为tail时候
                           (t = tail)   != null &&                 //tail不为null
                           (s = t.next) != null && // advance and retry         //tail后面一个也不为null
                           (s = s.next) != null && s != t);   //tail的后面的后面也不为null。即有节点的话,就一直往后面找
                }
                return p;                    //最终返回真实尾节点tail的前一个
            }
        }
    }

tryAppend方法的原理就是一直往后面找,直到找到真实tail时候,添加;如果添加失败则往后面继续找。

下面再看看awaitMatch的阻塞方法:

    private E awaitMatch(Node s, Node pred, E e, boolean timed, long nanos) {
        final long deadline = timed ? System.nanoTime() + nanos : 0L;            //获取超时时间。
        Thread w = Thread.currentThread();              //获取当前线程。
        int spins = -1; // initialized after first item and cancel checks         //默认自旋次数。
        ThreadLocalRandom randomYields = null; // bound if needed         当前线程的一个随机数。

        for (;;) {                               //循环。
            Object item = s.item;
            if (item != e) {                  // matched       匹配到了。
                // assert item != s;
                s.forgetContents();           // avoid garbage        避免垃圾回收。
                return LinkedTransferQueue.cast(item);
            }
            if ((w.isInterrupted() || (timed && nanos <= 0)) &&        //超时或者中断就取消。
                    s.casItem(e, s)) {        // cancel
                unsplice(pred, s);
                return e;
            }

            if (spins < 0) {                  // establish spins at/near front      //初始化自旋次数。
                if ((spins = spinsFor(pred, s.isData)) > 0)
                    randomYields = ThreadLocalRandom.current();
            }
            else if (spins > 0) {             // spin           //自旋次数组件,或者yield。
                --spins;
                if (randomYields.nextInt(CHAINED_SPINS) == 0)
                    Thread.yield();           // occasionally yield
            }
            else if (s.waiter == null) {             //初始化waiter。
                s.waiter = w;                 // request unpark then recheck
            }
            else if (timed) {                //是否需要park当前线程。
                nanos = deadline - System.nanoTime();
                if (nanos > 0L)
                    LockSupport.parkNanos(this, nanos);
            }
            else {
                LockSupport.park(this);
            }
        }
    }

awaitMatch也比较容易理解:

  • 如果匹配到了,那么就出队;
  • 如果中断,那么也取消,解除连接;
  • 自旋一定次数需要阻塞;
  • 并且超时性的parkNanos。

原理分析

总的来说,LinkedTransferQueue从代码上还是比上一个阻塞队列要简单些,但是它的设计方面却比较难理解。当然就是jdk下面那一大段的注释说明。

  Dual Queues是该队列的基础理论。此队列不进存放数据节点,也会存放请求节点。当一个线程试图放入一个数据节点,正好遇到一个请求数据的结点,会立刻匹配并移除该数据节点,对于请求节点入队列也是一样的。Blocking Dual Queues阻塞所有未匹配的线程,直到有匹配的线程出现。
  一个先入先出的dual queue实现是无锁队列算法M&S的变体。其包含两个指向字段:head指向一个匹配的结点,然后依次指向未匹配的结点,如果不为空。tail指向最后一个节点,或者null,如果队列为空。例如下图是一个包含四个元素的队列结构:
  这里写图片描述

 M&S算法易于扩展和保持(通过CAS)这些头部和尾指针。在dual队列中,节点需要自动维护匹配状态。所以这里需要一些必要的变量:对于数据模式,匹配需要将一个item字段通过CAS从非null的数据转成null,反之对于请求模式,需要从null变成data。一旦一个节点匹配了,其状态将不再改变。因此通常安排元素链表的前缀是0个或多个匹配节点,而后跟随0个或多个未匹配节点。如果不关心时间或空间的效率,通过从头指针开始遍历队列放入取出操作都是对的。CAS操作第一个未匹配节点匹配时的item,在下一个字段追加后一个节点。然而这是一个糟糕的想法,虽然其确实有好处,不需对head或tail进行原子更新。

  LinkedTransferQueue采取了一种折中的方案,介于实时更新head/tail和不更新head/tail之间的方法。该方法对有时候需要额外的遍历去定位第一个或最后一个未匹配的结点和减少开销及队列结点的竞争更新这两个方面进行了权衡。例如,一个可能出现的队列快照如下图:
  这里写图片描述

slack(head位置和第一个未匹配的结点的最大距离,尾结点类似)的最佳值是一个经验问题,发现在1~3之间在大部分平台是最佳的值。更大的值会增加内存命中开销和长遍历链表的风险,更小的值则会增加CAS的竞争开销。

  具体实现:使用一个基础的threshold来更新,slack为2(在xfer有所体现)。所以在当前位置超过第一个或最后一个节点2个距离以上的时候就会更新head/tail。出入队列操作都是通过xfer方法完成的,只需要不同的参数来表示操作。

参考文章:
1. https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/LinkedTransferQueue.html
2. https://www.cnblogs.com/lighten/p/7505355.html

你可能感兴趣的:(Java并发学习)