Java同步数据结构之LinkedTransferQueue
前言
LinkedTransferQueue是Java并发包中最强大的基于链表的无界FIFO阻塞传输队列。从JDK7开始出现,Doug Lea说LinkedTransferQueue是ConcurrentLinkedQueue、SynchronousQueue (公平模式下)、无界的LinkedBlockingQueues等的超集,这句话就已经说明了LinkedTransferQueue的特性了,首先LinkedTransferQueue是基于链表的无界FIFO阻塞队列,它是接口TransferQueue的实现类,而TransferQueue又继承了接口BlockingQueue,所以说它有无界的LinkedBlockingQueues的特性,它通过CAS实现,并且队列的head与tail指针如同ConcurrentLinkedQueue一样并不总是指向队列的头和尾节点,所以它有ConcurrentLinkedQueue的特性,最后它实现的接口TransferQueue定义的transfer接口方法实现了类似SynchronousQueue的特性,即生产者(调用transfer的线程)必须等到一个消费者(调用BlockingQueue.take()或定时poll的线程)接收指定的元素完成数据的单向传递才返回,否则阻塞等待。
我们知道SynchronousQueue虽然被设计成数据的单向传递工具,但是它其实并不是一个真正意义上的队列(对外部使用者来说),因为所有Collection的相关方法(包括迭代器)都将把SynchronousQueue当成一个空队列处理,然而LinkedTransferQueue却同时具有队列容量与SynchronousQueue的特性,这在某些时候是非常有用的,例如在消息传递应用程序中,生产者有时(使用TransferQueue的transfer方法)需要等待调用take或poll的消费者接收元素,而在其他时候又不需要等待接收就可以(通过put等BlockingQueue的方法)加入队列元素。还可以通过hasWaitingConsumer查询TransferQueue中是否有线程在等待数据。
TransferQueue接口
TransferQueue接口继承了BlockingQueue,它定义了如下几个自己特有的方法,TransferQueue被设计成类似SynchronousQueue的功能,即生产者可以等待消费者接收元素。
boolean tryTransfer(E e);
如果存在一个消费者已经在等待接收元素(通过BlockingQueue.take()或定时poll),则立即传输指定的元素,否则返回false而不入队该元素。
void transfer(E e);
如果存在一个消费者已经在等待接收元素(通过BlockingQueue.take()或定时poll),则立即传输指定的元素,否则等待直到该元素被一个消费者接收。
boolean tryTransfer(E e, long timeout, TimeUnit unit);超时版本的tryTransfer。
boolean hasWaitingConsumer();
如果至少有一个消费者在通过BlockingQueue.take()或定时poll等待接收元素,则返回true。返回值表示事件的瞬时状态。
int getWaitingConsumerCount();
返回通过BlockingQueue.take()或定时poll等待接收元素的消费者数量的估计值。返回值是对事件瞬间状态的近似,如果消费者已经完成或放弃等待,则返回值可能不准确。该值可能对监视和启发有用,但对同步控制没用。此方法的实现可能比hasWaitingConsumer()方法的实现要慢得多。
与其他阻塞队列一样,TransferQueue可能有容量限制。如果是这样,尝试的传输操作可能首先阻塞等待可用空间,然后阻塞等待消费者接收。注意,在容量为零的队列中,例如SynchronousQueue, put和transfer实际上是同义词。
LinkedTransferQueue
原理简介
JavaDoc中使用了大量的说明来阐述其整个设计想法与具体实现,从中可以看出LinkedTransferQueue采用基于松弛阈值的双队列实现,所谓双队列即它的节点既可以表示数据,也可以表示请求,这和SynchronousQueue的节点定义是一样的,甚至其内部节点的源码都与SynchronousQueue的公平模式下的节点字段一样,当然其实现元素传递的过程也与SynchronousQueue的原理差不多:当一个线程试图入队一个数据节点,但遇到一个请求节点时,它会与其完成“匹配”并出队,反之亦然。具体来说,对BlockingQueue的实现部分会使未被匹配的请求节点入队阻塞直到其它数据线程来与其完成匹配,而对TransferQueue的实现同步队列部分也会使未被匹配的数据节点入队阻塞直到其它请求线程来与其完成匹配,LinkedTransferQueue可以通过调用者传递的参数来同时对这两种模式进行支持。
而所谓的松弛阈值其实指,在对head、tail两个指针的维护时,允许它们距离第一个未匹配节点/最后一个节点的距离,ConcurrentLinkedQueue中也有相应的设计,即head不总是指向队列第一个节点,tail也不总是指向队列的最后一个节点,因为当维护(通过CAS)这些head和tail指针时,容易受到可伸缩性和开销限制,这种基于松弛阈值的策略减少了对队列指针更新的开销和争用,LinkedTransferQueue与ConcurrentLinkedQueue中该阈值都为2,即只要距离超过1就需要更新head/tail,这时候可以通过检查遍历指针的相等性来实现大于1的路径,除非队列只有一个元素,在这种情况下,我们将阈值保持为1。该松弛阈值的最佳取值是一个经验问题,在1-3的范围内使用非常小的常量在各种平台上工作得最好。较大的值会增加缓存未命中的成本和长遍历链(即,遍历时遇到无效节点需要从head重遍历)的风险,而较小的值会增加CAS争用和开销。
作为对松弛阈值技术的扩展,通过将无效节点的next指向自身(即自链接)限制了死链接的长度,这对垃圾回收也是一种优化考虑,然而这将使得遍历过程变得复杂化,在遍历过程中遇到这种自链接节点就表明当前线程落后于head的更新,遍历必须重新从head开始,另外试图从“tail”开始查找当前真正的尾节点的遍历可能也会遇到自链接,在这种情况下,它们也会重新从“head”开始遍历。
LinkedTransferQueue使head和tail在第一个节点进入队列之前都为null(即初始化状态),而在第一次入队节点时才初始化head和tail。这简化了其他一些逻辑,并提供了更有效的显式路径控制。
LinkedTransferQueue的所有入队/出队操作都由单个方法“xfer”处理,其参数指示是作为哪种形式的offer、put、poll、take还是transfer(每个可能都有超时),而整个出入队的过程由三个阶段也就是三个方法实现分别是xfer,tryAppend,awaitMatch。
阶段1. 尝试匹配一个存在的节点(xfer方法中的内部for循环)
- 从head开始,跳过已经匹配的节点,直到找到一个模式相反的未匹配节点,如果存在的话完成匹配并返回,如有必要,会将head更新到匹配节点的下一个节点(如果列表中没有其他未匹配的节点,则是节点本身),以保证松弛阀值不超过1。匹配过程中如果发现当前循环到的节点已经出队,则从head重新开始。
- 如果没有找到候选节点,并且是被poll,tryTransfer调用,则立即返回。
阶段2. tryAppend --- 尝试添加一个新节点
- 从当前tail指针开始,找到实际的最后一个节点,并尝试添加一个新节点(如果head为null,则创建第一个节点)。只有当节点的前驱节点已经匹配或具有相同的模式时,才可以添加节点。如果检测到其他情况,则说明在遍历期间有模式相反的节点进入了队列中,因此必须重新尝试阶段1,遍历和更新步骤与阶段1类似:在CAS失败时重试并检查过期。如果遇到自链接,则可以通过从当前head重新遍历,安全地跳转到列表中的节点。
- 如果节点成功入队,并且调用是异步而不是同步,则立即返回。
阶段3. awaitMatch --- 等待匹配或取消
- 等待另一个线程匹配节点,如果当前线程被中断或等待超时,则取消。在多处理器上,我们使用front-of-queue自旋:即如果一个节点是队列中第一个不匹配的节点,它会在阻塞之前自旋一会。不论发生何种情况,在阻塞之前,它都会尝试将当前“head”和第一个未匹配节点之间的任何节点断开链接。
- 阻塞前这种自旋策略极大地提高了竞争激烈的队列的性能。而且如果自旋足够快和短暂,对竞争较少的队列的性能影响也不大。在自旋期间,线程会检查它们的中断状态,并生成一个线程本地随机数,以决定是否偶尔执行Thread.yield出让CPU。这种随机出让CPU的方式有可能降低自旋对竞争繁忙的队列造成的影响。此外,针对后面的更多链式的自旋节点采用更小(1/2)的自旋,从而避免大量节点的交替自旋和阻塞。
对于内部已删除或由于中断/超时而取消节点的清理,通常采用将其前驱的next指向被移除节点的next来清理它。但有两种情况下,我们不能保证能够通过这种方式清理该节点使其不可达:①如果s队列的尾节点(即,其next为null),它被认为是入队节点的目标节点,所以我们只有在有新节点入队之后才能清理它。②如果其前驱是已经被匹配(或被取消)的节点,这时候其前驱可能已经从队列中断开了链接,并且更前面的可达节点可能依然指向s。如果不考虑这些因素可能将会导致无限数量的可能已删除的节点仍然可访问,虽然这种情况并不多见,但在实践中是真实可能发生的例如,当一系列的带短时间超时时间的poll调用重复超时时,由于存在一个不定时的take操作导致那些超时poll不会从队列中移除。当出现这种情况时,并不会总是重新遍历整个队列,以找到一个要取消节点的真实前驱节点(这对情况①是没有任何作用的),而是记录清理失败的估计次数(通过sweepvote变量),当这个次数超过一个阀值(“SWEEP_THRESHOLD”)时,才触发一次完整的清理。这个阀值(“SWEEP_THRESHOLD”)的选取也是一个经验值,它平衡了浪费精力和争用的可能性,而不是将无效节点长期保留在队列中的最坏情况界限。
源码解析
通过上面的原理简介其实已经把LinkedTransferQueue的原理说的很清楚了,接下来看源码会容易的多,首先看看其基础字段属性和节点内部类Node:
1 //是否是多处理器 2 private static final boolean MP = Runtime.getRuntime().availableProcessors() > 1; 3 4 //第一个等待入队节点在阻塞之前在多处理器上自旋的次数 5 private static final int FRONT_SPINS = 1 << 7; 6 7 //当一个节点前面已经有一个明显在自旋的节点时,在阻塞之前自旋的次数。该值是第一个自旋节点自旋次数的一半。 8 private static final int CHAINED_SPINS = FRONT_SPINS >>> 1; 9 10 //在必须清理队列之前要容忍的最大估计清理失败次数阀值(sweepvote),该值必须至少为2,以避免在删除尾节点时进行无用的清理。 11 static final int SWEEP_THRESHOLD = 32; 12 13 /** 队列的head,第一个节点入队之前该值为null */ 14 transient volatile Node head; 15 16 /** 队列的tail,第一个节点入队之前该值为null */ 17 private transient volatile Node tail; 18 19 /** 清理已删除节点失败的次数 */ 20 private transient volatile int sweepVotes; 21 22 // 字段的CAS方法 --- tail 23 private boolean casTail(Node cmp, Node val) { 24 return UNSAFE.compareAndSwapObject(this, tailOffset, cmp, val); 25 } 26 27 // 字段的CAS方法 --- head 28 private boolean casHead(Node cmp, Node val) { 29 return UNSAFE.compareAndSwapObject(this, headOffset, cmp, val); 30 } 31 32 // 字段的CAS方法 --- sweepVotes 33 private boolean casSweepVotes(int cmp, int val) { 34 return UNSAFE.compareAndSwapInt(this, sweepVotesOffset, cmp, val); 35 } 36 37 /* 38 * xfer方法中“how”参数的可能值。 39 */ 40 private static final int NOW = 0; // 非定时的poll和tryTransfer 41 private static final int ASYNC = 1; // 对offer, put, add方法使用 42 private static final int SYNC = 2; // 对transfer, take方法使用 43 private static final int TIMED = 3; // 定时版本的poll和tryTransfer 44 45 @SuppressWarnings("unchecked") 46 static <E> E cast(Object item) { //数据类型转换方法 47 // assert item == null || item.getClass() != Node.class; 48 return (E) item; 49 } 50 51 52 //创建初始为空的LinkedTransferQueue。 53 public LinkedTransferQueue() { 54 } 55 //创建一个最初包含给定集合元素的LinkedTransferQueue,按集合迭代器的遍历顺序添加。 56 public LinkedTransferQueue(Collection<? extends E> c) { 57 this(); 58 addAll(c); 59 } 60 61 //队列的节点类,允许在使用之后忘记节点数据item, 62 static final class Node { 63 final boolean isData; // 如果这是一个请求节点,则为false 64 volatile Object item; // 如果是数据节点item不为空,通过CAS进行交换该数据 65 volatile Node next; // 下一个节点引用 66 volatile Thread waiter; // 阻塞等待的线程 67 68 // 对字段next进行更新的CAS方法 69 final boolean casNext(Node cmp, Node val) { 70 return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val); 71 } 72 73 // 对节点数据item进行更新的CAS方法 74 final boolean casItem(Object cmp, Object val) { 75 // assert cmp == null || cmp.getClass() != Node.class; 76 return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val); 77 } 78 79 /** 80 * 构造一个新节点。使用普通写而不是volatile写,因为只有在通过CAS执行了casNext将其加入到队列中之后才需要被可见,而casNext本身就会顺带着完成item的可见性。 81 */ 82 Node(Object item, boolean isData) { 83 UNSAFE.putObject(this, itemOffset, item); 84 this.isData = isData; 85 } 86 87 /** 88 * 将next指向自身以避免垃圾存留,因为只会在casHead之后调用,所以这里也使用普通写。 89 */ 90 final void forgetNext() { 91 UNSAFE.putObject(this, nextOffset, this); 92 } 93 94 /** 95 * 将item指向自身,将waiter置为null,以避免在匹配和取消之后的对引用的持有(垃圾存留)。 96 * 使用普通写,因为有序性已经在其调用的上下文中得到保证:只有在volatile/atomic的item操作之后才会调用该方法, 97 * 同样,清理waiter要么在CAS操作之后,要么从阻塞唤醒之后(如果没有阻塞过,则没有任何影响)。 98 */ 99 final void forgetContents() { 100 UNSAFE.putObject(this, itemOffset, this); 101 UNSAFE.putObject(this, waiterOffset, null); 102 } 103 104 /** 105 * 如果该节点已经被匹配则返回true,包括由于取消而导致的伪造匹配。 106 */ 107 final boolean isMatched() { 108 Object x = item; 109 return (x == this) || ((x == null) == isData); 110 } 111 112 /** 113 * 如果这是一个未匹配的请求节点,则返回true。 114 */ 115 final boolean isUnmatchedRequest() { 116 return !isData && item == null; 117 } 118 119 /** 120 * 如果无法将一个给定模式的节点链接到该节点,则返回true,因为该节点未被匹配,并且具有与其相反的节点模式。 121 */ 122 final boolean cannotPrecede(boolean haveData) { 123 boolean d = isData; 124 Object x; 125 //模式相反 && 未取消 && 未匹配 126 return d != haveData && (x = item) != this && (x != null) == d; 127 } 128 129 /** 130 * 尝试人为匹配一个数据节点(由remove使用) 131 */ 132 final boolean tryMatchData() { 133 // assert isData; 134 Object x = item; 135 //是数据节点 && 未匹配和取消,清空数据 136 if (x != null && x != this && casItem(x, null)) { 137 LockSupport.unpark(waiter); //唤醒等待线程 138 return true; 139 } 140 return false; 141 } 142 143 private static final long serialVersionUID = -3375979862319811754L; 144 145 // Unsafe mechanics 146 private static final sun.misc.Unsafe UNSAFE; 147 private static final long itemOffset; 148 private static final long nextOffset; 149 private static final long waiterOffset; 150 static { 151 try { 152 UNSAFE = sun.misc.Unsafe.getUnsafe(); 153 Class<?> k = Node.class; 154 itemOffset = UNSAFE.objectFieldOffset 155 (k.getDeclaredField("item")); 156 nextOffset = UNSAFE.objectFieldOffset 157 (k.getDeclaredField("next")); 158 waiterOffset = UNSAFE.objectFieldOffset 159 (k.getDeclaredField("waiter")); 160 } catch (Exception e) { 161 throw new Error(e); 162 } 163 } 164 }
通过构造方法可见LinkedTransferQueue是无界队列,Node节点的字段isData、item、next、waiter与SynchronousQueue的公平模式下的节点字段一样。其它都很简单就不一一说明。
入队操作
LinkedTransferQueue提供了add、put、offer以及超时版本的offer三类方法,用于将元素插入队列中:
1 //将指定的元素插入到此队列的末尾。因为队列是无界的,所以这个方法永远不会阻塞。 2 public void put(E e) { 3 xfer(e, true, ASYNC, 0); 4 } 5 //超时版本的offer 6 public boolean offer(E e, long timeout, TimeUnit unit) { 7 xfer(e, true, ASYNC, 0); 8 return true; 9 } 10 //将指定的元素插入到此队列的末尾。由于队列是无界的,这个方法永远不会返回false。 11 public boolean offer(E e) { 12 xfer(e, true, ASYNC, 0); 13 return true; 14 } 15 //将指定的元素插入到此队列的末尾。由于队列是无界的,这个方法永远不会抛出IllegalStateException或返回false。 16 public boolean add(E e) { 17 xfer(e, true, ASYNC, 0); 18 return true; 19 }
LinkedTransferQueue的入队方法都是通过xfer(e, true, ASYNC, 0)实现,所以add、put、offer、超时offer这几个方法完全可以看作一个方法,它们没有任何区别,由于LinkedTransferQueue是无界的,不会阻塞,所以在调用xfer方法是传入的是ASYNC(异步),同时直接返回true.
出队操作
LinkedTransferQueue提供了take、poll以及超时版本的poll方法用于出列元素:
1 public E take() throws InterruptedException { 2 E e = xfer(null, false, SYNC, 0); 3 if (e != null) 4 return e; 5 Thread.interrupted(); 6 throw new InterruptedException(); 7 } 8 9 //超时版本的poll 10 public E poll(long timeout, TimeUnit unit) throws InterruptedException { 11 E e = xfer(null, false, TIMED, unit.toNanos(timeout)); 12 if (e != null || !Thread.interrupted()) 13 return e; 14 throw new InterruptedException(); 15 } 16 17 public E poll() { 18 return xfer(null, false, NOW, 0); 19 }
take是阻塞方法,必须拿到一个元素才返回所以传入 xfer的"how"参数为SYNC(同步),方法poll传入的NOW所以不论如何都会立即返回,毕竟它的定义是直接拿走队列的头部有效元素,如果没有就算了,超时版本的poll传入的TIMED.
数据传输操作
LinkedTransferQueue实现了TransferQueue的数据传递方法:
1 //传输指定的数据元素给一个消费者,阻塞直到成功。 2 //更精确地说,如果已经有消费者在等待接收指定的元素(通过take或超时版本的poll方法),则立即传输指定的元素,否则将指定的元素插入队列的末尾,并阻塞等待直到有消费者接收到该元素。 3 public void transfer(E e) throws InterruptedException { 4 if (xfer(e, true, SYNC, 0) != null) { 5 Thread.interrupted(); // failure possible only due to interrupt 6 throw new InterruptedException(); 7 } 8 } 9 10 //如果可能,立即将指定的数据元素传递给一个正在等待的消费者。 11 //更精确地说,如果已经有消费者在等待接收指定的元素(通过take或超时版本的poll方法),则立即传输指定的元素,否则返回false,而不需要将元素加入队列。 12 public boolean tryTransfer(E e) { 13 return xfer(e, true, NOW, 0) == null; 14 } 15 16 //如果可能在超时之前将元素传输给使用者,则将该元素传输给使用者。 17 //更准确地说,如果已经有消费者在等待接收指定的元素(通过take或超时版本的poll方法),则立即传输指定的元素,否则将指定的元素插入队列的末尾并阻塞等待直到有消费者接收到该元素。如果在指定的超时时间到底之后依然没有被消费者接收则立即返回false 18 public boolean tryTransfer(E e, long timeout, TimeUnit unit) 19 throws InterruptedException { 20 if (xfer(e, true, TIMED, unit.toNanos(timeout)) == null) 21 return true; 22 if (!Thread.interrupted()) 23 return false; 24 throw new InterruptedException(); 25 }
transfer是一个方法阻塞方法,所以传入 xfer的"how"参数为SYNC(同步),方法tryTransfer传入的NOW所以不论如何都会立即返回,超时版本的tryTransfer传入的TIMED.
核心方法1:E xfer(E e, boolean haveData, int how, long nanos)
通过上面几个核心方法的源码我们可以清楚的看到,最终都是调用xfer()方法,该方法接受四个参数,数据item或者是null的数据E,put操作为true、take操作为false的havaData,how(有四个值NOW, ASYNC, SYNC, or TIMED,分别表示不同的操作),超时纳秒数nanos。
1 /** 2 * 实现所有队列方法。见上面的解释。 3 * 4 * @param e 入队数据或null(执行take时) 5 * @param haveData true表示put操作,否则表示take操作 6 * @param how NOW, ASYNC, SYNC, or TIMED 7 * @param nanos 超时纳秒数,仅仅用于超时模式 8 * @return 返回匹配的数据item,否则返回参数e 9 * @throws NullPointerException 如果是put操作但是e为null则抛出空指针异常 10 */ 11 private E xfer(E e, boolean haveData, int how, long nanos) { 12 if (haveData && (e == null)) 13 throw new NullPointerException(); 14 Node s = null; // the node to append, if needed 15 16 retry: 17 for (;;) { // restart on append race 18 19 //从head开始找一个未匹配的互补节点尝试进行匹配交换数据 20 for (Node h = head, p = h; p != null;) { // find & match first node 21 boolean isData = p.isData; 22 Object item = p.item; 23 // p节点未被匹配,并且节点模式合符规定 24 if (item != p && (item != null) == isData) { 25 // 当前节点与待处理数据模式相同,不能匹配,重新开始 26 if (isData == haveData) 27 break; 28 // 到这里说明当前节点与待处理数据模式互补,故进行匹配即交换数据 29 if (p.casItem(item, e)) { 30 for (Node q = p; q != h;) { 31 Node n = q.next; // update by 2 unless singleton 32 //维护head与第一个未匹配节点之间的阈值不超过1 33 //如果当前节点已经是最后一个节点了则head指向该节点,否则指向该节点的下一个节点 34 if (head == h && casHead(h, n == null ? q : n)) { 35 h.forgetNext(); //让原head的next指向自身,形成自链接 36 break; 37 } 38 //如果head已经变化,或者更新head失败,则重新查看head与第一个未匹配节点的距离 39 //注意这里通过 q = h.next改变了q的指向 40 if ((h = head) == null || 41 (q = h.next) == null || !q.isMatched()) 42 break; // if条件成立说明head距离第一个未匹配节点没有超过1,所以不需要更新head 43 } 44 45 //匹配完成之后唤醒被阻塞在当前节点的线程,返回节点数据 46 LockSupport.unpark(p.waiter); 47 return LinkedTransferQueue.<E>cast(item); 48 } 49 } 50 //如果当前节点已经被匹配或者匹配失败则继续看下一个节点 51 //如果当前节点已经出队了,则从head重新开始 52 Node n = p.next; 53 p = (p != n) ? n : (h = head); // Use head if p offlist 54 } 55 //到这里说明没有找到可以匹配的节点 56 //how不为NOW(put,offer,add,take、超时poll,transfer,超时tryTransfer),说明需要入队 57 if (how != NOW) { // No matches available 58 if (s == null) 59 s = new Node(e, haveData); //创建节点实例 60 Node pred = tryAppend(s, haveData); //尝试加入队尾,返回其前驱节点 61 if (pred == null) //前驱为null,说明有与其互补的未匹配节点入队 62 continue retry; // 这个时候需要重新尝试匹配(万一刚刚有模式互补的节点入队呢) 63 if (how != ASYNC) //how 不是异步即是同步或者超时等待(take,超时poll,transfer,超时tryTransfer),说明需要阻塞等待 64 return awaitMatch(s, pred, e, (how == TIMED), nanos); 65 } 66 return e; //不需要等待直接返回数据e(put,offer,超时offer,add入队之后返回;poll,tryTransfer不入队返回) 67 }
xfer方法的核心算法就是寻找互补未匹配节点进行匹配,匹配成功就唤醒对应节点的线程然后返回,匹配失败就考虑是不是需要入队,以及需不需要阻塞等待的问题,具体逻辑如下:
- 从head开始依次往后尝试找到一个与当前操作互补的未匹配节点进行匹配。匹配成功之后在返回之前有两件事需要考虑做:①维护松弛阀值即保证head与第一个未匹配节点之间的距离不超过1一个节点;②唤醒阻塞在匹配节点的线程。
- 如果步骤1没有找到可以匹配的节点,就看要不要将当前操作(生产者或消费者)构造新节点入队等待,如果how是"NOW"(poll,tryTransfer)则不需要入队所以立即返回数据e;
- 否则若步骤2判断需要入队(put,offer,add,take、超时poll,transfer,超时tryTransfer),则构造新节点通过tryAppend方法入队。
- 步骤3三入队完成之后再看需不需要等待,如果how不是“ASYNC” (take,超时poll,transfer,超时tryTransfer)则需要阻塞等待,否则入队完成之后就可以立即返回e了(put,offer,超时版本offer,add)。
从xfer方法可以看出,①所有入队操作都不会被阻塞,但在入队之前会尝试从队列头部开始往后找与其互补的未匹配节点进行匹配交换数据,如果成功了就不需要入队从而立即返回,否则需要入队链接到队尾,当然在入队过程中如果有互补节点入队导致入队失败,也会继续尝试匹配,否则入队成功或继续尝试匹配还是失败之后则立即返回。②take、超时poll、transfer,超时tryTransfer需要入队并阻塞等待。③poll,tryTransfer这种非阻塞方法不需要入队,不论步骤1成功失败都将立即返回。
核心方法2:Node tryAppend(Node s, boolean haveData)
从上面的xfer方法实现可以看到在需要入队节点的时候需要执行另一个核心方法tryAppend,s表示当前需要入队的节点本身,haveData指示当前节点的模式。
1 /** 2 * 尝试将节点s追加为尾部。 3 * 返回值: 4 * 1. 队列为空,刚刚入队的s是队列中唯一的节点,返回s本身 5 * 2. 队列不为空,成功将s链接到最后一个节点p之后,返回s的前驱p 6 * 3. 队列不为空,但是队列中存在与其互补的未匹配节点,返回null 7 */ 8 private Node tryAppend(Node s, boolean haveData) { 9 for (Node t = tail, p = t;;) { // move p to last node and append 10 Node n, u; // temps for reads of next & tail 11 //队列为空,则直接将s设置成head,返回s本身 12 if (p == null && (p = head) == null) { 13 if (casHead(null, s)) 14 return s; // initialize 15 } 16 //当前节点p是一个模式互补且未被匹配的节点则不能链接到该节点之后 17 //因为它完全可以和节点s完成匹配使它们都返回 18 else if (p.cannotPrecede(haveData)) 19 return null; 20 //当前节点p不是实际的最后一个节点,继续循环寻找最后一个节点 21 else if ((n = p.next) != null) 22 p = p != t && t != (u = tail) ? (t = u) : // tail被更新了则取新的tail 23 (p != n) ? n : null; // 取p的下一个节点或者若p已经失效重新从head开始 24 //p是最后一个节点,将s链接到它的下一个节点 25 //如果被其它线程抢先入队则p指向其next继续循环 26 else if (!p.casNext(null, s)) 27 p = p.next; 28 else { 29 //到这里说明成功将s链接到p的next,根据需要维护尾节点的松弛阀值不超过1 30 if (p != t) { // update if slack now >= 2 31 while ((tail != t || !casTail(t, s)) && //tail还没被更新则更新指向新的尾节点s 32 (t = tail) != null && 33 (s = t.next) != null && // advance and retry 34 (s = s.next) != null && s != t); 35 } 36 return p; //返回s的前驱 37 } 38 } 39 }
tryAppend的逻辑很简单,主要就是从tail开始寻找最后一个模式相同的节点,然后将其链接到它的next。在遍历过程中:
- 如果队列为空,则直接将节点s设置成head,返回s本身。
- 如果发现一个与s模式互补的未匹配节点则返回null,在xfer中将会继续尝试匹配。
- 如果遍历到的当前节点不是队列的最后一个节点(tail可以不指向最后一个节点),则将遍历指针指向下一个节点(如果发现tail被更新了就重新从tail出开始遍历)。
- 否则p是最后一个节点,尝试将s链接到它的next,如果失败则将遍历指针指向p的next继续遍历。成功则根据需要维护tail节点的松弛阀值,使其距离队列最后一个节点的距离不超过一个节点。最后返回s的前驱节点p。
tryAppend的返回值有以下几种情况:
- 队列为空,刚刚入队的s是队列中唯一的节点,返回s本身
- 队列不为空,成功将s链接到最后一个节点p之后,返回s的前驱p
- 队列不为空,但是队列中存在与其互补的未匹配节点,返回null
核心方法3:E awaitMatch(Node s, Node pred, E e, boolean timed, long nanos)
1 /** 2 * 自旋/出让CPU/阻塞,直到节点s被匹配或调用者放弃。 3 * 返回匹配的数据,或者e如果在中断或超时发生时依然还没有匹配成功 4 */ 5 private E awaitMatch(Node s, Node pred, E e, boolean timed, long nanos) { 6 final long deadline = timed ? System.nanoTime() + nanos : 0L; 7 Thread w = Thread.currentThread(); 8 int spins = -1; // initialized after first item and cancel checks 9 ThreadLocalRandom randomYields = null; // bound if needed 10 11 for (;;) { 12 Object item = s.item; 13 //数据已经发生变化说明已经被匹配了 14 if (item != e) { // matched 15 // assert item != s; 16 s.forgetContents(); // 消除垃圾存留 17 return LinkedTransferQueue.<E>cast(item); //返回被匹配的数据项 18 } 19 //如果发生了中断或超时,则取消节点,即将数据item指向自身,返回e 20 if ((w.isInterrupted() || (timed && nanos <= 0)) && 21 s.casItem(e, s)) { // cancel 22 unsplice(pred, s); //断开s节点的链接 23 return e; 24 } 25 26 //到这里说明节点没有发生异常,要进行阻塞 27 //先获取阻塞前的自旋次数,以及随机出让CPU的随机数实例 28 //关于自旋次数: 29 if (spins < 0) { 30 if ((spins = spinsFor(pred, s.isData)) > 0) 31 randomYields = ThreadLocalRandom.current(); 32 } 33 //自旋,过程中随机出让CPU 34 else if (spins > 0) { // spin 35 --spins; 36 if (randomYields.nextInt(CHAINED_SPINS) == 0) 37 Thread.yield(); // occasionally yield 38 } 39 //自旋结束,做阻塞前准备,将线程设置到waiter 40 else if (s.waiter == null) { 41 s.waiter = w; // request unpark then recheck 42 } 43 //已经做好了阻塞准备,根据不同情况分别做无限期阻塞或者定时阻塞 44 else if (timed) { 45 nanos = deadline - System.nanoTime(); 46 if (nanos > 0L) 47 LockSupport.parkNanos(this, nanos); 定时阻塞 48 } 49 else { 50 LockSupport.park(this); 无限期阻塞 51 } 52 } 53 } 54 55 //返回具有给定前驱节点和数据模式的节点s的自旋次数 56 private static int spinsFor(Node pred, boolean haveData) { 57 if (MP && pred != null) { //是多处理器才需要自旋 58 //这种情况什么时候发生,说明什么?我确实没有搞懂 59 if (pred.isData != haveData) 60 return FRONT_SPINS + CHAINED_SPINS; 61 // 前驱已经被匹配了,即当前节点是第一个自旋节点,自旋次数为FRONT_SPINS 62 if (pred.isMatched()) 63 return FRONT_SPINS; 64 // 前驱也处于自旋状态,则自旋次数为其一半 65 if (pred.waiter == null) 66 return CHAINED_SPINS; 67 } 68 return 0; //单核CPU不需要自旋,自旋次数为0 69 }
在xfer中节点入队之后,如果how不是ASYNC则需要入队等待,即执行awaitMatch方法,该方法的逻辑很简单,主要分三个阶段:
- 第一阶段:进行状态检测,是否已经被匹配,是否已经被中断、超时,已经被匹配则说明匹配之后被对方唤醒所以直接返回匹配的数据即可,中断或超时则返回自身e;
- 第二阶段:若第一阶段的情况未发生,则根据情况进行不同的自旋次数,在自旋过程中随机出让CPU, 自旋完成过程中若第一阶段的情况依然没有发生,则准备阻塞(将当前线程引用赋值给节点的waiter字段);
- 第三阶段:第二阶段完成之后,根据不同的情况做无限期阻塞或定时阻塞,等待唤醒或超时自动醒来。
在awaitMatch的第一阶段中,若发生了超时或中断,在返回之前还需要断开该节点的链接,即unsplice方法:
1 /** 2 * 断开已经取消或删除的节点s与其前驱节点的链接 3 * 4 * @param pred a node that was at one time known to be the 5 * predecessor of s, or null or s itself if s is/was at head 6 * @param s the node to be unspliced 7 */ 8 final void unsplice(Node pred, Node s) { 9 s.forgetContents(); // 防止垃圾引用持有 10 /* 11 * 1. 如果前驱依然指向s,尝试断开与s的链接。 12 * 2. 如果操作失败(由于s是尾节点或者前驱已经断开了),并且前驱和s都不是head也没有出队,则积累失败次数 13 * 3. 当失败次数累计到临界值就进行清理 14 */ 15 if (pred != null && pred != s && pred.next == s) {//前驱的依然next指向s 16 Node n = s.next; 17 //s是尾节点 或者 (为何pred.casNext(s, n)成功了还要执行下面这些逻辑???)前驱已经被匹配 18 if (n == null || 19 (n != s && pred.casNext(s, n) && pred.isMatched())) { 20 // 看是否是head或将要成为新的head,根据需要更新head指向第一个未匹配节点 21 for (;;) { 22 Node h = head; 23 if (h == pred || h == s || h == null) 24 return; // 是头节点或者队列为空,直接返回 25 if (!h.isMatched()) //head未被匹配,则不需对head进行处理 26 break; 27 Node hn = h.next; 28 if (hn == null) 29 return; // 队列为空,返回 30 if (hn != h && casHead(h, hn)) //使head指向第一个未匹配节点 31 h.forgetNext(); 32 } 33 //重新检查节点是否已经出队,若没有对失败次数进行累计, 34 //当失败次数达到临界值SWEEP_THRESHOLD,执行sweep进行清理 35 //sweep就是从head开始遍历清除队列中那些已经匹配过的节点 36 if (pred.next != pred && s.next != s) { 37 38 for (;;) { 39 int v = sweepVotes; 40 if (v < SWEEP_THRESHOLD) { 41 if (casSweepVotes(v, v + 1)) 42 break; 43 } 44 else if (casSweepVotes(v, 0)) { 45 sweep(); 46 break; 47 } 48 } 49 } 50 } 51 } 52 }
说实话,unsplice中的第二个if条件没有看明白,n == null 表示s是尾节点,作为下次入队节点的前驱,所以不能断开s的链接这个可以理解,后面的条件n != s && pred.casNext(s, n) && pred.isMatched() 就没有看懂了,为什么pred.casNext(s, n) 成功将s与其前驱的节点断开了还要执行里面的逻辑???不过if块里面的逻辑很简单:for (;;) 维护head使其在必要是被更新指向第一个未匹配节点;对断开链接的失败次数进行累计,当达到临界值SWEEP_THRESHOLD,执行sweep进行对整个队列的无效节点进行清理。
内部节点移除方法:boolean remove(Object o)
1 //从此队列中删除指定元素的单个实例(如果存在)。 2 public boolean remove(Object o) { 3 return findAndRemove(o); 4 } 5 6 private boolean findAndRemove(Object e) { 7 if (e != null) { 8 for (Node pred = null, p = head; p != null; ) { 9 Object item = p.item; 10 //如果是数据节点 11 if (p.isData) { 12 //与数据节点进行数据比较,找到之后进行人为的伪匹配, 13 //并唤醒对应的阻塞线程,然后尝试断开节点与其前驱的链接,返回true 14 if (item != null && item != p && e.equals(item) && 15 p.tryMatchData()) { 16 unsplice(pred, p); 17 return true; 18 } 19 } 20 //第一个节点若不是数据节点,则表示队列中都是请求节点。直接返回false 21 else if (item == null) 22 break; 23 //到这里说明队列中是数据节点,但是当前遍历节点数据不是目标数据 24 //更新遍历指针到下一个节点,或者从head重新开始遍历(当前节点已经失效) 25 pred = p; 26 if ((p = p.next) == pred) { // stale 27 pred = null; 28 p = head; 29 } 30 } 31 } 32 return false; 33 }
内部节点移除方法,就是找到队列中第一个数据item与其相等(item.equals(o))的节点并移除,通过源码可见移除是通过伪匹配实现的,即伪造成被请求线程匹配,然后唤醒对应的阻塞线程,尝试断开该节点与其前驱的链接。
boolean hasWaitingConsumer()方法
1 //是否存在等待的消费者 2 public boolean hasWaitingConsumer() { 3 return firstOfMode(false) != null; 4 } 5 6 //返回队列中第一个未匹配的指定模式的节点 7 private Node firstOfMode(boolean isData) { 8 for (Node p = head; p != null; p = succ(p)) { 9 if (!p.isMatched()) //未匹配 10 return (p.isData == isData) ? p : null;//模式相同则返回节点,否则返回null 11 } 12 return null; 13 }
如果队列中存在至少一个未匹配的数据请求节点则返回true,否则返回false。
getWaitingConsumerCount() 和size()方法
1 //返回队列中未匹配的数据节点个数 2 public int size() { 3 return countOfMode(true); 4 } 5 //返回队列中未匹配的请求节点个数 6 public int getWaitingConsumerCount() { 7 return countOfMode(false); 8 } 9 10 //返回队列中指定模式的未匹配节点的个数 11 private int countOfMode(boolean data) { 12 int count = 0; 13 for (Node p = head; p != null; ) { 14 if (!p.isMatched()) {//未匹配的节点 15 //第一个节点模式相反就表示没有该模式的节点 16 if (p.isData != data) 17 return 0; 18 //否则计数器加1,直到增大到Integer.MAX_VALUE为止 19 if (++count == Integer.MAX_VALUE) // saturated 20 break; 21 } 22 //更新遍历指针 23 Node n = p.next; 24 if (n != p) 25 p = n; //p的next 26 else { //p已经无效了,从head重新开始数 27 count = 0; 28 p = head; 29 } 30 } 31 return count; 32 }
getWaitingConsumerCount方法返回队列中未匹配的请求节点的个数,size与其相反,返回的是队列中未匹配的数据节点的个数,如果超过了Integer.MAX_VALUE个,就返回Integer.MAX_VALUE;size只是一个瞬时值,因为队列随时都在异步变化。
另外一些方法:
peek()方法返回的是队列中的第一个未匹配的数据节点的数据;isEmpty() 方法判断的是队列中是否存在未匹配的数据节点;remainingCapacity()返回的总是Integer.MAX_VALUE,因为队列的无界的;drainTo方法转移所有或最多多少个的队列中的数据元素到指定的集合中,该集合不能是当前队列本身;contains(o)查看队列中是否存在数据item与其相等的数据节点,不论是否已经被匹配;
不能保证批量操作addAll、removeAll、retainAll、containsAll、equals和toArray是原子执行的。例如,与addAll操作并发操作的迭代器可能只能查看到部分添加的元素。
迭代器
LinkedTransferQueue的迭代器与ConcurrentLinkedQueue结构类似,但是比ConcurrentLinkedQueue的实现要复杂的多,但是都是在创建迭代器实例的时候就已经拿到了第一个节点以及节点数据,每一次执行next的时候又准备好下一次迭代的返回对象,但是LinkedTransferQueue在迭代的过程中会对队列中的无效节点进行清理,由于LinkedTransferQueue队列是双队列,既可以存放数据节点,也可以存放请求节点,但同一时刻,队列中只能是一种节点,其迭代器只会对数据节点进行遍历,由于LinkedTransferQueue对节点的清理比起ConcurrentLinkedQueue来说要复杂的多,ConcurrentLinkedQueue只需要将节点数据item清空即可,但是LinkedTransferQueue则需要通过其前驱节点来对节点进行清理,所以其迭代器还需要对前驱节点lastPred
进行维护,以用于调用迭代器的remove方法使用。LinkedTransferQueue的迭代器的remove方法会进行伪匹配,唤醒对应的阻塞线程,并执行unsplice对节点进行清理。其代码有点复杂难懂,就不贴源码咯。
可拆分迭代器Spliterator
LinkedTransferQueue实现了自己的可拆分迭代器内部类LTQSpliterator,但是其可拆分迭代器与ConcurrentLinkedQueue的实现几乎一致,只不过它只对队列中的item有效的数据节点进行处理,tryAdvance获取队列中第一个item有效的数据节点的数据做指定的操作,forEachRemaining循环遍历当前队列中item有效的数据节点的数据做指定的操作源码都很简单,就不贴代码了,至于它的拆分方法trySplit,其实和ConcurrentLinkedQueue拆分方式是一样的,代码都几乎一致,它不是像ArrayBlockingQueue那样每次分一半,而是第一次只拆一个元素,第二次拆2个,第三次拆三个,依次内推,拆分的次数越多,拆分出的新迭代器分的得元素越多,直到一个很大的数MAX_BATCH(33554432) ,后面的迭代器每次都分到这么多的元素,拆分的实现逻辑很简单,每一次拆分结束都记录下拆分到哪个元素,下一次拆分从上次结束的位置继续往下拆分,直到没有元素可拆分了返回null。
总结
LinkedTransferQueue是Java同步包队列(非双端队列)中最强大的队列,它是一种双队列,即节点存在数据节点,请求节点这种互补模式,这两种模式代表可以完成数据交换,但大多时候其实现的集合方法在没有特别强调的情况下都是对有效数据节点的数据进行处理而非请求节点,因为请求节点携带的数据为null,它同时满足了ConcurrentLinkedQueue那样的非阻塞(CAS)实现的无界链表队列实现,它的所有入队方法包括带有超时时间的offer都是相同的实现,所以调用任意一个都是相同的效果,最主要的是它还实现了类似SynchronousQueue那样的数据传递的阻塞方法,通常来说无界队列的入队方法都是非阻塞的,因为无容量限制,只要内存足够就可以无限制的往里面加入元素,它无法确定丢进去的数据是否/什么时候被消费掉的,但LinkedTransferQueue实现类似SynchronousQueue的功能即TransferQueue接口特有的transfer/超时tryTransfer方法提供了这种特性,它是一个阻塞入队方法,直到入队数据被消费或超时才返回,另外还能对正在等待消费线程的个数进行估计,它可以说是ConcurrentLinkedQueue + SynchronousQueue的综合体,其代码实现所以也相对的复杂的多,但一般来说不需要这种传输特性的时候使用ConcurrentLinkedQueue 完全足以,虽然据说LinkedTransferQueue的性能要比ConcurrentLinkedQueue 好很多,但我没有去去验证。使用的时候就按照这个优先顺序吧:LinkedTransferQueue/ConcurrentLinkedQueue -> LinkedBlockingQueues -> ArrayBlockingQueue.