ThreadPoolExecutor以BlockingQueue存储待执行任务,包括SynchronousQueue、LinkedBlockingQueue和ArrayBlockingQueue,今天的目的是源码角度深入研究SynchronousQueue。
之后计划是继续研究LinkedBlockingQueue和ArrayBlockingQueue,搬开所有绊脚石之后再开始线程池。
基本概念#BlockingQueue
BlockingQueue是SynchronousQueue的爹,他们的祖先是Queue,所以他们都会遵从Queue的一些基本逻辑:比如按顺序存入数据、按顺序(FIFO或者LIFO)取出数据,都是从队首(head)获取数据,FIFO队列新数据从队尾入队、LIFO队列队列新数据入队首。
对于BlockingQueue,我们还是认真看一下他的javaDoc:
BlockingQueue是一个特殊的Queue:当获取数据的时候阻塞等待队列非空,存入数据的时候阻塞等待队列可用(有界队列未满)。
BlockingQueue的方法(获取数据或者存入数据)不能立即成功、但是将来某一时间点可能会成功的情况下,有四种不同的处理方式:一种是抛出异常,第二种是返回特殊值(空值或者false),第三种是无限期阻塞当前线程知道成功,第四种是阻塞等待设定的时间。
具体如下表:
Throws exception | Special value | Blocks | Times out | |
Insert | add add(e) | offer offer(e) | put put(e) | offer(Object, long, TimeUnit) offer(e, time, unit) |
Remove | #remove remove() | #poll poll() | #take take() | #poll(long, TimeUnit) poll(time, unit) |
Examine | #element element() | #peek peek() | not applicable | not applicable |
BlockingQueue不接受空值null,尝试写入null会抛出异常,因为BlockingQueue用null表示操作失败。
BlockingQueue可以是有界的也可以是无界的,有界队列维护剩余容量属性remainingCapacity,超出该属性后会阻塞写入操作。
无界队列没有容量限制,始终维护remainingCapacity为Integer.MAX_VALUE
BlockingQueue是线程安全的,BlockingQueue的实现类的方法都是原子操作、或者通过其他并发控制方式实现线程安全性。
基本概念SynchronousQueue
SynchronousQueue是一个每次写入数据都必须等待其他线程获取数据(反之亦然)的BlockingQueue。SynchronousQueue没有容量的概念、一条数据都不能存储。你不能对SynchronousQueue队列执行peek操作因为只有执行remove操作才能获取到数据,只有其他线程要remove数据的时候你才能插入数据,你也不能进行迭代因为他根本就没有数据。队列的队首数据就是第一个写入数据的线程尝试写入的数据,如果没有写入线程则队列中就没有数据可获取,poll()方法会返回null。对于集合类的其他方法、比如contains,SynchronousQueue的返回等同于空集合。
SynchronousQueue不允许空(null)元素。
通过构造参数设置fairness为true提供公平队列,公平队列遵从FIFO(先进先出)。
基本概念 Transferer
Transferer是SynchronousQueue的内部类,他的JavaDoc也很长:
Transferer扩展实现了W. N. Scherer III和M. L.Scott在"Nonblocking Concurrent Objects with Condition Synchronization"中描述的双栈(dual stack)或者双队列(dual queue)算法。
后进先出(Lifo)栈用来实现非公平模式,先进先出(Fifo)队列用来实现公平模式。两者的性能是一样的,一般情况下Fifo支持大吞吐量、Lifo maintains higher thread locality(抱歉,没搞懂什么意思)。
dual queue(或dual stack)在任一时间要么持有数据(写入操作提供的)、要么持有请求(获取数据的请求),向正好持有数据的队列请求数据、或者向持有请求的队列写入数据被称之为"fulfill"。最有趣的特性是对队列的任何操作都能知道队列当时处于什么状态,因此操作不必要上锁。
queue和stack都扩展自虚拟类Transferer,Transferer定义了一个方法transfer,可以同时实现put和take功能。把put和take统一在一个transfer方法中的原因是dual数据结构使得put和take操作是对称操作,所以两个方法的大部分代码都可以被合并。
好了,有关JavaDoc描述的特性就到这里了,SynchronousQueue的JavaDoc很不容易理解,代码也是。
开始分析源码:
- 构造函数:决定SynchronousQueue底层数据结构是用Queue还是Stack
- 由于SynchronousQueue是比较特殊的:不存储数据的(JavaDoc提到过),所以需要明确的是相关集合方法的返回也相应比较特殊,比如size=0,isEmpty=true等等,这部分源码就不看了,特别简单
- 队列存、取数据的方法,最终调用的都是Transferer的tranfer方法,所以我们主要就看这个方法
SynchronousQueue构造方法
提供一个有参构造方法接收一个布尔量fair,我们前面说过,Queue是公平的、Stack是非公平的,所以fair=true的话创建TransferQueue,否则创建TransferStack。
TransferStack#SNode
TransferStack(TransferQueue也一样)的源码虽然不多,但是必须首先了解清楚他的数据结构,否则不太容易读懂。
节点SNode:也就是存储到栈内的内容,注意我这里没有说存储在栈内的数据而是说内容,是因为TransferStack的特殊性导致说数据容易引起误解:栈内有两种类型的节点,一种是“data”,可以理解为“生产者”放到栈内等得消费者消费的数据,另一种是“request”,可以理解为消费者的消费请求,也就是说请求和数据都会入栈,都属于“节点”。
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
Object item; // data; or null for REQUESTs
int mode;
next:下一节点。
match:当前节点的匹配节点,比如一个请求数据的Request节点入栈后,正好有一个data节点入栈,他们两个如果匹配成功的话,match就是对方节点。
waiter:如果当前节点即使在自旋等待后仍然没有被匹配,比如一个请求线程发送获取数据的请求后,该请求会以请求节点(Request节点)入栈,始终没有数据送进来,则当前节点的waiters就记录为当前线程,之后当前线程自己挂起,等待匹配。这个等待匹配的过程是被动的,只能被另外一个data线程送进来的data节点匹配,匹配之后data线程通过Request节点的waiters获取到其对应的线程后唤醒该线程。
item:data类的节点,记录送进来的待消费的数据,Request类的节点,item为null。
mode:当前节点的mode,有三个mode:data mode表示当前节点是数据节点(生产者发来的),Request mode表示当前节点是请求节点(消费者发来的),还有一个比较特殊的mode是:匹配中的数据节点或匹配中的请求节点,这个mode后面分析tranfer代码的时候再说。
head:头节点,栈结构嘛,入栈节点始终是头节点,也只有头节点具有正常出栈的权限。
SNode提供了几个原子性的操作:
- casNext:cas方式替换当前节点的下一节点
- tryCancel;这个实现比较特殊:当前节点的match如果为null的话则将match指向自己。用这种方式表示该节点被calcel
- casHead:cas的方式修改头节点,其实就是入栈或出栈操作
TransferStack#transfer
E transfer(E e, boolean timed, long nanos) {
SNode s = null; // constructed/reused as needed
//如果e为null的话就是REQUEST操作,否则就是DATA操作
int mode = (e == null) ? REQUEST : DATA;
for (;;) {
//取头节点(首节点)h
SNode h = head;
//空栈,或者首节点mode与当前操作的mode相同,说明当前节点与首节点不可能匹配了
if (h == null || h.mode == mode) { // empty or same-mode
//时间到,等不了了
if (timed && nanos <= 0) { // can't wait
//首节点被calcel了
if (h != null && h.isCancelled())
//首节点出栈
casHead(h, h.next); // pop cancelled node
//既然等不及了,就返回null
else
return null;
//否则,当前节点入栈,如果入栈成功,s就是首节点了
} else if (casHead(h, s = snode(s, e, h, mode))) {
//调用awaitFulfill阻塞等待匹配节点
SNode m = awaitFulfill(s, timed, nanos);
//阻塞等待调用结果如果是s的话,说明s被取消了
if (m == s) { // wait was cancelled
clean(s);
return null;
}
//否则就是阻塞等待后匹配成功了,那么判断如果头节点不空并且下一节点是s的话
//说明除了等来一个匹配节点之外,没有其他节点加入,那么这一对儿匹配节点都出栈
if ((h = head) != null && h.next == s)
casHead(h, s.next); // help s's fulfiller
//成功匹配,可以返回了
return (E) ((mode == REQUEST) ? m.item : s.item);
}
//否则,存在头节点并且当前节点mode不同可以匹配,并且头节点尚未被其他线程匹配
} else if (!isFulfilling(h.mode)) { // try to fulfill
//如果头节点已经被取消,则出栈
if (h.isCancelled()) // already cancelled
casHead(h, h.next); // pop and retry
//否则当前节点以FULLFILLING模式入栈,s变为首节点
else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
//一直循环直到成功匹配或栈内的等待节点突然消失
for (;;) { // loop until matched or waiters disappear
//m为s的下一节点
SNode m = s.next; // m is s's match
//m空,说明以前的等待节点突然消失,比如等待节点超时取消
if (m == null) { // all waiters are gone
//清空栈
casHead(s, null); // pop fulfill node
//清空s,重新进入主循环
s = null; // use new node next time
break; // restart main loop
}
//否则,可以开始匹配了,mn为m的下一节点,为出栈做好准备
SNode mn = m.next;
//如果m和s能匹配成功,则m和s都出栈,返回结果
if (m.tryMatch(s)) {
casHead(s, mn); // pop both s and m
return (E) ((mode == REQUEST) ? m.item : s.item);
//匹配没有成功,这种情况应该是m超时取消掉了,则m出栈
} else // lost match
s.casNext(m, mn); // help unlink
}
}
//否则,栈首节点处于匹配中FULLFILLING的状态(其他线程正在匹配但是尚未完成)
//这种情况下,新来的节点不入栈,先协助完成栈首节点的匹配
} 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 {
//否则,去匹配首节点h和他的下一节点m,如果匹配成功了则h和m出栈
//这种情况下是不需要返回,因为是协助其他线程完成匹配,自己的匹配任务尚未开始呢...,其他线程如果获得执行权之后,会发现已经有人帮助他完成匹配了,所以会很快返回结果
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
}
}
}
}
TransferStack#awaitFulfill
awaitFulfill的作用是通过自旋、或者阻塞当前线程来等待节点被匹配。
3个参数:
SNode s:等待匹配的节点。
booean timed:true则表示限时等待。
long nanos:限时等待时长。
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 (;;) {
//如果当前线程被中断,则calcel掉当前节点:将s的match指向自己
if (w.isInterrupted())
s.tryCancel();
//自旋过程中完成匹配,则直接返回匹配节点
SNode m = s.match;
if (m != null)
return m;
//如果是显示匹配并且匹配超时,则cancel掉s节点
if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
s.tryCancel();
continue;
}
}
//自旋时长未到则继续自旋
if (spins > 0)
spins = shouldSpin(s) ? (spins-1) : 0;
//完成自旋后记录当前线程
else if (s.waiter == null)
s.waiter = w; // establish waiter so can park next iter
//阻塞当前线程
else if (!timed)
LockSupport.park(this);
else if (nanos > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanos);
}
}
TransferStack#clean
clean方法在transfer方法中调用,如果当前节点在等待匹配的过程中已经被cancel掉的话。
代码不贴出了,基本逻辑就是首先清掉s及s关联的线程(s=null,s.waiter=null),然后清查并清理掉被cancel掉的head节点(从head到s之后的第一个未被cancel掉的节点逐个检查),直到确保栈的head节点正常(未被calcel)。
然后从head开始、到s之后的第一个未被cancel掉的节点逐个检查,如果有节点被标记为cancel则该节点出栈。
执行完成之后,不止是s节点被清理,栈内从head节点开始直到s节点的下一个未被cancel掉的节点之间的节点,如果被cancel掉的话,全部会被清理出栈。
小结
基于TransferStack的SynchronousQueue的源码就分析完成了,感觉不对照代码逐行说明的话,就很不容易说清楚TransferStack的transfer、awaitFulfill方法的代码逻辑,所以就采用在源码中逐行注释的方式来说明了。
篇幅原因,TransferQueue下次再说!
Thanks a lot!
上一篇 Runable和Callable的区别?你必须要搞清楚Thread以及FutureTask!
下一篇 BlockQueue - 基于TransferQueue的SynchronousQueue
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。