SynchronousQueue也是阻塞队列中的一种,它用在线程池中可以让线程池动态调整线程数量,任务多它就创建多点线程去执行任务,任务少它就会把多余的线程释放掉。这在一定程度上提高了并发的吞吐量,但是需要注意的是系统资源是否承受的起线程的扩容。
我们从代码层面去理解,可以把插入操作和取出操作分别封装为节点A、节点B。那么可以把SynchronousQueue说成是配对管道,管道中只会保留一种节点,要么全是A,要么全是B。当全是节点A的管道遇到节点B时,就完成了一个配对,管道会释放掉一个节点A。
也可以把SynchronousQueue当作是一种手把手传递东西,一个物体得经过两个手才算传递完成,就是我手上有一个物体,得有下一个人接收才算完。也可以比作我大A股,假如我手上有不错的票,得有接盘的韭菜买,我才会赚到钱。
一、代码结构
从如下类关系中可以看到,SynchronousQueue继承了AbstractQueue,说明它也是集合框架中的成员,BlockingQueue的子类基本上都是集合框架成员。
从构造器中看到,它有一个boolean的入参,用来指定是否公平模式。true的话会使用TransferQueue队列作为管道,它可以保证先进先出。false使用TransferStack,提供后进先出的实现方式。
public class SynchronousQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
public SynchronousQueue() {
this(false);
}
public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
/** 传递模式,0表示来接收的,也就是获取操作*/
static final int REQUEST = 0;
/** 1代表本身有东西,需要传递给他人,对应的是插入操作 */
static final int DATA = 1;
/** 2代表已经配对成功了 */
static final int FULFILLING = 2;
}
二、出入队列操作
SynchronousQueue有个很特殊的地方就是,它的出入队列其实最后的逻辑都是在同一个方法中,下面我们来看看它的两种实现方式。
1. 后进先出实现方式
这也叫做非公平方式,先来看看它的入队列方法。
// put方法插入队列,其实也可以说是传递物体,
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// 调用transfer方法
if (transferer.transfer(e, false, 0) == null) {
Thread.interrupted();
throw new InterruptedException();
}
}
// 无阻塞入队列
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
// 也是直接调用了transfer()方法
return transferer.transfer(e, true, 0) != null;
}
// 有阻塞出队列
public E take() throws InterruptedException {
E e = transferer.transfer(null, false, 0);
if (e != null)
return e;
Thread.interrupted();
throw new InterruptedException();
}
// 无阻塞出队列,通过参数true和0控制
public E poll() {
return transferer.transfer(null, true, 0);
}
E transfer(E e, boolean timed, long nanos) {
SNode s = null; // constructed/reused as needed
// e为空说明是获取操作,对应REQUEST。否则是DATA
int mode = (e == null) ? REQUEST : DATA;
for (;;) {
SNode h = head;
// 头节点为空,或者头节点模式跟当前操作一致,说明没有配对成功。
// 这里只需要判断头节点即可,因为管道中只会保留一种类型的节点
if (h == null || h.mode == mode) { // empty or same-mode
// 判断是否需要等待,put是不需要等待的,take方法需要等待
if (timed && nanos <= 0) { // can't wait
if (h != null && h.isCancelled())
// 这里是替换掉失效的头节点
casHead(h, h.next); // pop cancelled node
else
return null;
}
// 这一行可以说明它就是是后进先出了,直接把头节点替换为当前节点
else if (casHead(h, s = snode(s, e, h, mode))) {
// 等待配对,这个方法这里可能会阻塞,如果返回的节点m等于当前节点,
// 那么说明配对失败,直接返回null,下面分析这个方法
SNode m = awaitFulfill(s, timed, nanos);
// 阻塞被唤醒,如果m == s 说明配对不成功
if (m == s) { // wait was cancelled
// 断开当前节点
clean(s);
return null;
}
if ((h = head) != null && h.next == s)
// 这一步是帮忙释放节点的,跟下方的”代码标记A”有同样的功效
casHead(h, s.next); // help s's fulfiller
return (E) ((mode == REQUEST) ? m.item : s.item);
}
}
// 这个分支是准备配对的
else if (!isFulfilling(h.mode)) { // try to fulfill
// 节点取消配对,也可以说是出入队列失败了
if (h.isCancelled()) // already cancelled
casHead(h, h.next); // pop and retry
else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
for (;;) { // loop until matched or waiters disappear
SNode m = s.next; // m is s's match
if (m == null) { // all waiters are gone
// 能够配对的节点为空了,所以设置头节点也为空
casHead(s, null); // pop fulfill node
s = null; // use new node next time
break; // restart main loop
}
SNode mn = m.next;
// m和s做配对,tryMatch方法后面有介绍
if (m.tryMatch(s)) {
// 代码标记A
casHead(s, mn); // pop both s and m
// 这里的返回很奇妙了,他就是出入队列可以复用同一个方法的原理所在
return (E) ((mode == REQUEST) ? m.item : s.item);
} else // lost match
// 如果配对失败,说明有其他节点配对了m,我们从mn开始接着配对。因为这里没加锁,全靠CAS去完成配对,所以失败是有可能的。
s.casNext(m, mn); // help unlink
}
}
} else { // help a fulfiller
/* 进到这个分支,说明有线程正在第二个分支,头节点正在配对中,那么我们就帮助第二个分支,加速配对。
这个分支的作用其实可以忽略的,我这里也搞不懂为啥需要这个分支。
有可能是担心第二个分支的for语句被操作系统限制运行了,由于线程调度的原因,正在执行第二分支的线程可能被挂起了。
当另外的线程进入到第三个分支就可以加速后面的配对了。*/
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
}
}
}
}
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 (;;) {
// 如果被中断了,那么取消配对,s.match为自己
if (w.isInterrupted())
s.tryCancel();
// 返回配对结果,如果配成功的话,match非空不等于自己
SNode m = s.match;
if (m != null)
return m;
if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
s.tryCancel();
continue;
}
}
// 用spins控制轮询多次看是否配对成功
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);
// spinForTimeoutThreshold=1000,即大于这个数才需要调用可以倒计时的阻塞方法
else if (nanos > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanos);
}
}
// 当出入队列失败时会断开当前节点
void clean(SNode s) {
// 置空当前节点item
s.item = null; // forget item
s.waiter = null; // forget thread
SNode past = s.next;
if (past != null && past.isCancelled())
past = past.next;
// 从head开始遍历,释放past之前的失效的头节点,用下一个节点替换为新的节点
SNode p;
while ((p = head) != null && p != past && p.isCancelled())
casHead(p, p.next);
// 从新的head开始直到当前节点的下一个节点,释放掉失效的节点
while (p != null && p != past) {
SNode n = p.next;
if (n != null && n.isCancelled())
p.casNext(n, n.next);
else
p = n;
}
}
/** 生成一个新的节点 */
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;
}
// 判断是否配对成功
static boolean isFulfilling(int m) { return (m & FULFILLING) != 0; }
// 这个方法挺重要的,有可能是被transfer方法的第二,第三个分支同时调用,谁先调用就唤醒阻塞方法,然后返回true。后调用的方法也没关系,还有个match==s作为是否匹配成功的判断
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;
}
2. 先进先出实现方式
后进先出是往队列尾部插入节点,队列头部取出节点。
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
// 队列为空,或者管道中都是相同模式的节点
if (h == t || t.isData == isData) { // empty or same-mode
QNode tn = t.next;
// 尾节点被更改了,不一致读,重新轮询
if (t != tail) // inconsistent read
continue;
// 重新设置尾节点
if (tn != null) { // lagging tail
advanceTail(t, tn);
continue;
}
// 如果阻塞超时的话返回null
if (timed && nanos <= 0) // can't wait
return 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
// 等待配对,后面分析这个方法
Object x = awaitFulfill(s, e, timed, nanos);
// 被取消了配对,那么就要清除该节点,后面分析该方法
if (x == s) { // wait was cancelled
clean(t, s);
return null;
}
// s在队列中
if (!s.isOffList()) { // not already unlinked
// 如果t是头节点,则头节点替换为s
advanceHead(t, s); // unlink if head
// x不为空,说明当前操作是取数
if (x != null) // and forget fields
s.item = s;
s.waiter = null;
}
return (x != null) ? (E)x : e;
} else { // complementary-mode
// 进到这个分支是符合配对条件的,从头节点配对
QNode m = h.next; // node to fulfill
// 不一致读,重新轮询
if (t != tail || m == null || h != head)
continue; // inconsistent read
Object x = m.item;
// 如下条件满足一个,则替换头节点
if (isData == (x != null) || // m 已经配对了
x == m || // m 被取消了
!m.casItem(x, e)) { // cas配对失败
advanceHead(h, m); // 设置新的头节点,然后重新轮询
continue;
}
// 走到这里说明成功配对,替换头节点为下一个节点
advanceHead(h, m);
// 唤醒配对的节点
LockSupport.unpark(m.waiter);
return (x != null) ? (E)x : e;
}
}
}
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);
Object x = s.item;
// x!=e 说明是配对成功,或者被取消了
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);
}
}
void clean(QNode pred, QNode s) {
s.waiter = null; // forget thread
// 轮询条件,s是pred的下一个节点
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;
// sn等于s说明s节点被取消了,casNext成功的话说明清除s成功,
if (sn == s || pred.casNext(s, sn))
return;
}
// cleanMe可以理解为最近在哪个节点的后面入队列失败了,比如A是尾节点,B节点入队列失败了,那么cleanMe就是A节点。总之就是如果节点被取消了,那么cleanMe就是被取消节点的前一个节点
QNode dp = cleanMe;
if (dp != null) { // Try unlinking previous cancelled node
QNode d = dp.next;
QNode dn;
// 如下4种情况需要把cleanMe重新设置为null,第4种情况其实d就是之前被取消的节点,需要清除掉
if (d == null || // 1.d为null 或者
d == dp || // 2.d不在队列了 或者
!d.isCancelled() || // 3.d没有被取消 或者
(d != t && // 4.d不是尾节点
(dn = d.next) != null && // 并且d的后继节点不为空
dn != d && // 并且d在队列中
dp.casNext(d, dn))) // 并且清除d成功,
casCleanMe(dp, null);
// 如果cleanMe节点是pred,那么就可以返回了
if (dp == pred)
return; // s is already saved node
} else if (casCleanMe(null, pred))
// 到这里其实是没有清除掉这个取消的节点,而是做了延迟清除,在前面代码里做了清除,从头节点开始清的
return; // Postpone cleaning s
}
}
三、总结SynchronousQueue的特质
- 对列没有容量的概念,也就是无界的,调用size()方法返回0。
- 不能删除节点,remove()方法都是返回false。
- peek()方法永远都是返回null。这也好理解,队列中保存的只是操作本身,而不是通常意义上的节点。
- 迭代器方法返回都是空的迭代器。
- 出入队列没有用加锁机制,直接用CAS完成。
- put()方法和take()方法,如果配对不成功的话会一直阻塞挂起,直到配对成功。