SynchronousQueue其实就是LinkedTransferQueue的升级版,相同的是它们都作为生产者和消费者交互的通道,可以直接让生产者和消费者打交道。不同的是,SynchronousQueue做的更彻底,不去支持无关的共有操作(比如size()
),只提供必要的入队出队方法。并且,SynchronousQueue提供了两种逻辑结构,栈和队列。
理解LinkedTransferQueue是搞懂SynchronousQueue的前提。
JUC框架 系列文章目录
abstract static class Transferer<E> {
abstract E transfer(E e, boolean timed, long nanos);
}
这里的transfer
方法与LinkedTransferQueue的不同。按照LinkedTransferQueue的话,不管是data node还是request node,如果transfer
成功返回的是创建节点时的item的相反值,如果transfer
失败返回的是创建节点时的item的相同值(比如创建了data node的线程,data node的item初始是非null的,如果交易成功,则返回null;如果交易失败,则返回非null),相反相同指的是 null 或 非null。
但SynchronousQueue的transfer
方法则不一样,不管是data node还是request node,如果transfer
成功返回的是非null,如果transfer
失败返回的是null。从子类实现中,我们可以看到这一点。
内部逻辑是栈的形式,让节点入队出队都从链表的同一端操作。
static final class SNode {
volatile SNode next; // 单链表的next指针
volatile SNode match; // 如果交易成功,那么交易先到的node的match成员,会指向交易后到的node
volatile Thread waiter; // 阻塞前保存当前线程,用来被unpark
Object item; // data node为非null;request node为null
int mode; // 有四种情况,下面介绍
item
不需要是volatile
的,因为创建节点之后总是会有CAS操作。
根据mode的值,有四种节点。
static final int REQUEST = 0;
static final int DATA = 1;
static final int FULFILLING = 2;
mode == REQUEST
即0b00
。交易先到的request node。mode == DATA
即0b01
。交易先到的data node。mode == FULFILLING | REQUEST
即0b10
。交易后到的request node。交易后到的都属于fulfilling node。mode == FULFILLING | DATA
即0b11
。交易后到的data node。交易后到的都属于fulfilling node。我们称request node和data node是两个相反模式的节点。
volatile SNode head; // 因为是栈结构,所以只需要用一个指针来保存栈顶
没有找到构造器,所以是默认构造器。初始时head为null。
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;
// 队列为空或队首模式相同(这说明队列中只有模式相同的节点)
// 注意,h.mode == mode已经排除掉了两种fulfilling node(data的、或request的)
if (h == null || h.mode == mode) {
//如果是计时操作,且传入时间参数<=0(一次尝试)
if (timed && nanos <= 0) { // 非计时操作或计时操作已经超时
if (h != null && h.isCancelled())
casHead(h, h.next); // 通过修改head出队取消节点
else
return null;
//1. 如果不是计时操作(同步操作)
//2. 如果是计时操作,且传入时间参数>0(同步带超时操作)
} else if (casHead(h, s = snode(s, e, h, mode))) {
SNode m = awaitFulfill(s, timed, nanos);//返回的是s的匹配对象
if (m == s) { // 如果匹配对象指向自身,说明当前线程被取消(中断或超时)
clean(s); // 尝试将s从栈中移除
return null;
}
// 如果匹配对象不是指向自身,说明当前线程等到了匹配对象的到来
if ((h = head) != null && h.next == s)
casHead(h, s.next); // 将匹配的一对节点出队
return (E) ((mode == REQUEST) ? m.item : s.item);
}
//队首为相反模式节点、或两种fulfilling node
//如果不是fulfilling node,那么队首为相反模式节点
} else if (!isFulfilling(h.mode)) { // 那么进行配对
if (h.isCancelled()) // 如果是取消节点
casHead(h, h.next); // 移除取消节点
else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {//尝试入栈用来匹配的fulfill节点
for (;;) { // loop until matched or waiters disappear
SNode m = s.next;
if (m == null) { // 如果发现没有匹配对象
casHead(s, null); // 清空整个栈
s = null; // 清空s因为旧s的成员不对
break; // 下一次循环将作为普通节点再入队
}
SNode mn = m.next; //链表结构s -> m -> mn
if (m.tryMatch(s)) {//匹配成功
casHead(s, mn); // 将匹配的一对节点出队
return (E) ((mode == REQUEST) ? m.item : s.item);
} else // 如果m是个取消节点
s.casNext(m, mn); // 移除这个取消节点
}
}
//剩下的情况都是fulfilling node
} else { // 那么帮忙出队匹配的一对
SNode m = h.next;
if (m == null) // 如果发现没有匹配对象
casHead(h, null); // 清空整个栈
else {
SNode mn = m.next;
if (m.tryMatch(h)) // 进行匹配
casHead(h, mn); // 将匹配的一对节点出队
else // 如果m是个取消节点
h.casNext(m, mn); // 移除这个取消节点
}
}
}
}
之前讲过节点有4种mode,现在transfer
方法就根据这4种mode分为了三种处理情况:
timed == true && nanos <= 0
。从字面意思就是这是个计时操作但却等0纳秒,其实就是只做一次尝试,如果只能找到相同模式节点的话,那么当前线程就不可能匹配成功,那就只能返回null代表失败了。timed == false
。不是计时操作,那就是一个同步操作。这种情况,如果只能找到相同模式节点的话,那么让当前线程创建节点入队,阻塞等待匹配的对方到来。从awaitFulfill
函数返回只能因为 匹配成功、被中断。timed == true && nanos > 0
。从字面意思就是一个正常的计时操作。这种情况,如果只能找到相同模式节点的话,那么让当前线程创建节点入队,等待匹配的对方到来。从awaitFulfill
函数返回只能因为 匹配成功、被中断、超时。这种加入fulfilling节点的手法类似于ConcurrentSkipListMap源码中的marker。在ConcurrentSkipListMap中,要删除链表中一个节点,不是直接将其移除,而是在被删除节点之前插入一个marker,当marker插入成功后,才将marker和被删除节点一起从链表中移除,这种做法保证了无锁实现能正确执行。而fulfilling节点的作用其实和marker是一样的。
整个执行过程能够保证链表中的非fulfilling节点肯定只有一种,要么是request node,要么是data node。
之前提到transfer
方法的返回值是成功返回非null,失败返回null。但是不管当前线程在交易中先到还是后到,执行的总是return (E) ((mode == REQUEST) ? m.item : s.item)
,这里我解释一下为什么。
从上图可知,其实是因为不管哪种情况,s
总是指向当前线程创建的节点,m
总是指向匹配对方创建的节点,而这两个节点一个是request node一个是data node,既然是这样的话,根据s
的模式就能知道这一对节点中哪个是data node,然后返回data node的item即可。所以永远执行return (E) ((mode == REQUEST) ? m.item : s.item)
。
在交易成功后,双方线程在尝试出栈时其实是做的同一个操作,先到线程执行的casHead(h, s.next)
,和后到线程执行的casHead(s, mn)
,从上图也可知,这两个CAS操作的两个参数其实是一模一样的,所以这两个CAS操作只有一个能成功,另一个直接失败就好。这种类似的,互相帮忙式的操作很多,比如clean(s)
和s.casNext(m, mn)
,都是在移除取消节点,前者被移除节点s
就是当前线程创建的,后者被移除节点m
不是当前线程创建的,而这后者就属于在帮忙。
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
,这个函数可以使得创建出来的节点可以被重复利用。
此函数返回当前线程创建节点的匹配节点。不过也可能返回的是自身节点,如果当前线程被中断或超时。总之,此函数只会返回 非null值。
SNode awaitFulfill(SNode s, boolean timed, long nanos) {//s肯定是当前线程创建的节点
final long deadline = timed ? System.nanoTime() + nanos : 0L;
Thread w = Thread.currentThread();
//计算出自旋次数
int spins = (shouldSpin(s) ?//当head还是s,或head是一个fulfiller节点时,shouldSpin返回true
(timed ? maxTimedSpins : maxUntimedSpins) : 0);
for (;;) {
if (w.isInterrupted())//如果被中断,则取消自身
s.tryCancel();
SNode m = s.match;
if (m != null)
return m; //有可能是正常被匹配到了,也有可能因为自身被取消。
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)//自旋次数为0后,这次循环先放置当前线程
s.waiter = w;
//自旋次数为0,且当前线程已经放置好了
else if (!timed)//如果是非超时操作,无限阻塞
LockSupport.park(this);
else if (nanos > spinForTimeoutThreshold)//如果是超时操作,执行超时版本阻塞
LockSupport.parkNanos(this, nanos);
//如果是超时操作,但时间太短,那么即使spins为0,也不停自旋直到时间消耗完
}
}
shouldSpin
),先进行自旋,因为当前线程可能在自旋过程中就发现自己被匹配了。Thread
成员。park
进行阻塞。自旋次数为0后,当前循环先放置当前线程作为s
成员,下一次循环再检查一次if (m != null)
看看是否被匹配了,尽可能避免不必要的阻塞,尽量在自旋过程中就发现自身节点被匹配到了。
boolean shouldSpin(SNode s) {
SNode h = head;
//中间的h == null只是严谨的防止空指针异常
return (h == s || h == null || isFulfilling(h.mode));
}
shouldSpin
函数返回真说明当前线程有必要自旋。
h == s
说明当前线程还在栈顶,那么继续自旋等待。s
正在被匹配当前,那么也继续自旋等待。h != s
且不是fulfill节点,这说明没有必要等了,因为要匹配也是先匹配栈顶节点。此函数尝试从栈中移除掉s节点,就算s节点就是head,也会将其移除。
void clean(SNode s) {//s肯定是个取消节点
s.item = null; // forget item
s.waiter = null; // forget thread
SNode past = s.next;//找到一个s的后继
if (past != null && past.isCancelled())//如果后继是取消节点
past = past.next;//则获得后继的后继
// 从head开始清除取消节点,直到遇到非取消节点或者past
SNode p;
while ((p = head) != null && p != past && p.isCancelled())
casHead(p, p.next);
//结束循环时,p可能为:
//1. null
//2. past(这说明past之前都是取消节点)
//3. 第一个非取消节点
// 这个循环遇到past就停止,过程中一直在移除取消节点,当遇到past说明s已经被移除了
// 注意这个循环只能清除p以后的取消节点,p本身如果是取消节点则无能为力
while (p != null && p != past) {
SNode n = p.next;
if (n != null && n.isCancelled())
p.casNext(n, n.next);
else
p = n;
}
}
p
,这个p
可能就是past,也可能是非past的一个非取消节点。p
进行遍历直到遇到past,遍历过程一直在移除取消节点。原理就是,如果遍历已经遇到了past,那么作为取消节点的s
就一定已经被移除掉了。最坏情况下,s
处于栈底,那么需要遍历整个链表。
之前提到,在并发情况下,有可能别的线程已经帮忙把s
移除了,所以遍历过程中如果以s
作为寻找标准,有可能永远也找不着。根据上图可知,如果在遍历过程中发现了s
的后继,那么也说明了s
被移除了。所以此函数使用了past这种东西。
内部逻辑是队列的形式,让节点入队出队从链表的各自两端操作。
整体上和LinkedTransferQueue很像,交易后到的一方不会真正加入fulfill节点,而是直接修改交易先到的节点的item域。但本队列初始时有dummy node。
static final class QNode {
volatile QNode next; // 单链表的next指针
volatile Object item; // 创建的是request node,为null;创建的是data node,为非null
volatile Thread waiter; // 阻塞前保存当前线程,用来被unpark
final boolean isData; // 两种模式,request node或data node
注意,不再需要match
成员了,因为TransferQueue
实际操作中并没有fulfill节点存在,而是直接修改匹配对方节点的item。
节点只有两种:
isData
为true。isData
为false。称上面两种节点模式相反。
节点的item域各个时期不同:
data node:
item
域为非null。item
域为null。item
域指向自身。request node:
item
域为null。item
域为非null。item
域指向自身。 transient volatile QNode head;//队首
transient volatile QNode tail;//队尾
transient volatile QNode cleanMe;//clean方法中使用,用来保存 需要延后移除的节点的前驱
TransferQueue() {
QNode h = new QNode(null, false); // initialize to dummy node.
head = h;
tail = h;
}
构造器创建一个dummy node,虽然它和一个正常的request node一样,但执行过程通过head == tail
来判断队列为空,所以不必担心。
队列为空时,head和tail指向同一个节点。真正的节点永远是head后继。
同样的,transfer
方法在匹配成功时返回非null值,匹配失败时返回null值。
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) // 如果看到了队列未初始化的值
continue; // 继续循环
if (h == t || t.isData == isData) { // 因为有dummy node,所以h == t代表队列空;队尾是相同模式
QNode tn = t.next;
if (t != tail) // 发现tail已经更新,那么继续循环
continue; // 这里如果不继续,接下来的t.casNext(null, s)也会失败的
if (tn != null) { // 虽然t为tail但它却有后继,说明遇到了别的线程添加节点后还没
advanceTail(t, tn); // 更新tail的中间状态。帮忙更新tail。
continue;
}
if (timed && nanos <= 0) // 如果是一次尝试版本,且发现队尾都是相同模式
return null; // 说明尝试失败,返回null
//其他情况就是,同步版本和超时版本
if (s == null) // 节点只创建一次
s = new QNode(e, isData);
if (!t.casNext(null, s)) // 入队失败,被别的线程抢先了
continue;
//入队成功
advanceTail(t, s); // 更新tail为最新入队节点
Object x = awaitFulfill(s, e, timed, nanos);
if (x == s) { // 当前线程的节点被取消而唤醒(中断或超时)
clean(t, s);
return null;
}
// 当前线程的节点是正常匹配而唤醒
if (!s.isOffList()) { // 如果s还没有自链接
advanceHead(t, s); // 使head从t变成s
if (x != null) // 如果awaitFulfill返回非null,说明匹配对方是生产者
s.item = s; // s节点为消费者,但s的item已变成非null,所以要释放掉这个引用
s.waiter = null; // 线程对象的引用一定要释放
}
return (x != null) ? (E)x : e;
// 队尾是相反模式
} else {
QNode m = h.next; // 如果队尾是相反模式,说明head后继也是相反模式
if (t != tail || m == null || h != head) // 队列结构已经和刚才看到的不一样了
continue;
Object x = m.item;
if (isData == (x != null) || // 参数isData与匹配对方节点的item域情况相同,说明对方已经被匹配
x == m || // item域指向自身,说明匹配对方节点被取消
!m.casItem(x, e)) { // 修改匹配对方节点的item域失败,说明对方已经 被匹配或取消
advanceHead(h, m); // 由于真正节点在head之后,所以head移动到m相当于出队m
continue; // 以上三种情况,m节点都不能被当前线程匹配了,需要继续循环
}
//执行到这里,说明上面的m.casItem(x, e)成功了,即配对成功
advanceHead(h, m); // 匹配成功后,也使得m出队
LockSupport.unpark(m.waiter); // 唤醒交易先到的一方
return (x != null) ? (E)x : e;
}
}
}
处理过程更加简单,因为交易后到的一方不需要添加fulfill节点,而是直接匹配:
timed == true && nanos <= 0
。做一次尝试,但既然现在队列中只有相同模式节点,那么尝试匹配失败,直接返回null。timed == false
。不是计时操作,那就是一个同步操作。虽然现在队列中只有相同模式节点,那么让当前线程创建节点入队,阻塞等待匹配的对方到来。从awaitFulfill
函数返回只能因为 匹配成功、被中断。timed == true && nanos > 0
。计时操作。那么让当前线程创建节点入队,有时间限制得阻塞等待匹配的对方到来。从awaitFulfill
函数返回只能因为 匹配成功、被中断、超时。整个执行过程能够保证链表中,属于刚创建状态的node肯定都是相同模式的。
上面两种情况,都是执行return (x != null) ? (E)x : e
,这能保证总是返回 匹配一对中的data node的初始item域。
x
都代表交易对方的初始item。
if (h == t || t.isData == isData)
的else
分支。x
就是交易对方节点的初始item域。另外,它会将交易对方节点的item域,修改为自己节点的item(虽然它并没有真正创建节点)。if (h == t || t.isData == isData)
分支。当匹配成功后从awaitFulfill
唤醒,返回x
是对方节点的item(上一条解释了)。当自身取消后从awaitFulfill
唤醒,返回x
是自己节点的this。e
就是自身的item。就是当初创建节点用的item。x
代表交易对方的初始item,e
就是自身的item。那么二者中肯定有一个是data node的初始item,那么返回其中的非null就好了。互相帮忙式的CAS操作也有,这些操作只有一次会成功:
if (h == t || t.isData == isData)
分支里的advanceTail(t, tn)
和advanceTail(t, s)
,这两个CAS都是帮忙把刚入队队尾但还没来得及更新tail的节点更新为tail。advanceHead(t, s)
和advanceHead(h, m)
。前者是,发现自己被正常匹配,然后更新head。后者是,发现head后继被正常匹配或取消,然后更新head。不管是正常匹配还是取消自身,都不应该让item域再持有引用,以免内存泄漏。
tryCancel
(在awaitFulfill
中执行的),item域指向自身,不管哪种节点(request or data),都释放了引用。if (x != null) s.item = s
,前面讲过x
代表交易对方的初始item,说明交易对方是生产者,自身节点s
是消费者,但由于正常匹配,自身节点s
的item域已经从null变成非null,所以也需要释放引用。相反情况则不用执行。 void advanceHead(QNode h, QNode nh) {
if (h == head &&
UNSAFE.compareAndSwapObject(this, headOffset, h, nh))
h.next = h; // forget old next
}
Object awaitFulfill(QNode s, E e, boolean timed, long nanos) {
/* 整体思想和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);//尝试把item域从初始的e变成this
Object x = s.item;
if (x != e) //不管是因为取消,还是因为正常匹配,这个都成立
//如果是正常匹配,x肯定与e相反(这里指null 和 非null 相反)
return x;
if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
s.tryCancel(e);
continue;
}
}
if (spins > 0)//这里不用再检查head.next == s了,因为这是队列结构,后来的节点肯定在s的后面
--spins;
else if (s.waiter == null)
s.waiter = w;
else if (!timed)
LockSupport.park(this);
else if (nanos > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanos);
}
}
此函数尝试从队列中移除s节点,但如果s是tail,此次调用函数不会执行移除动作,下次一定。
void clean(QNode pred, QNode s) {
s.waiter = null; // forget thread
while (pred.next == s) { // Return early if already unlinked
QNode h = head;
QNode hn = h.next; // 从head开始吸收后面的取消节点
if (hn != null && hn.isCancelled()) {
advanceHead(h, hn);
continue;
}
QNode t = tail; // 在使用前才读取tail,尽量保证读到最新
if (t == h) // 如果head == tail,那么队列为空
return;
QNode tn = t.next;
if (t != tail) // 发现tail已经更新
continue;
if (tn != null) { // tail没有更新,但tail有后继,这是个中间状态,帮忙更新tail
advanceTail(t, tn);
continue;
}
if (s != t) { // If not tail, try to unsplice
QNode sn = s.next;
// 如果s已经出队了(next指向自身)
// 如果将pred -> s -> sn结构修改为 pred -> sn
if (sn == s || pred.casNext(s, sn))
// 两种情况都说明s已经出队了
return;
}
//执行到这里,说明s就是tail,这次调用函数不能删除s节点了,下次一定
QNode dp = cleanMe;
if (dp != null) { // 发现cleanMe不为null,先移除cleanMe的后继
QNode d = dp.next; // d就是需要被移除的节点
QNode dn;
//结构为dp -> d -> dn
if (d == null || // d已经被移除
d == dp || // d已经被出队
!d.isCancelled() || // d应该是个取消节点的,但这里不是,说明已经被移除了
(d != t && // 接下来三行检查dp -> d -> dn的结构
(dn = d.next) != null &&
dn != d &&
dp.casNext(d, dn))) // 如果符合结构,那么尝试改成dp -> dn,即移除d节点
casCleanMe(dp, null); // 如果CAS成功,说明cleanMe后继移除成功
//一般情况下,下一次循环将执行casCleanMe(null, pred),保存好cleanMe后离开
if (dp == pred) // 上面过程只是帮上一次调用擦屁股,这一次因为不能删除s节点,
return; // 所以也需要保存好cleanMe再离开函数。如果发现dp就是pred,那说明cleanMe已经保存好了
} else if (casCleanMe(null, pred)) //如果cleanMe没被占领,占领它后直接return
return; // 之后再调这个函数会删除cleanMe的后继的
}
}
函数比较好理解,主要在于当发现s节点为tail时,此次调用不会移除掉它,而是留到下一次调用此函数再清理。因为下一次调用s就不再是队尾tail了。
casCleanMe(null, pred)
把需要移除节点的前驱暂时保存起来。casCleanMe(null, pred)
把需要移除节点的前驱暂时保存起来。之所以不移除tail的原因–concurrency-interest
这个原因concurrency-interest上面的大佬已经给予我解释了,比如说,如果当前链表为A -> B -> C(tail) -> null
,而且C节点需要被移除,那么链表应该变成A -> B(tail) -> null
。那么现在有几个问题:
A -> B(tail) -> null
,需要两个CAS操作,但这不可能是原子性的。A -> B -> null
和C(tail) -> null
。而这又会带来两个问题:
总之,不移除tail可以避免很多问题,也可以说移除tail根本没法正确实现。
SynchronousQueue完全没有容量的概念了,很多常用操作比如isEmpty()/size()/clear()/remove(Object)/contains(Object)/peek()
都不支持了。可以用的公共方法只有:
put(E)/offer(E, long, TimeUnit)/offer(E)
take()/poll(long, TimeUnit)/poll()
size()
总是返回0。transfer
方法,在返回值上:如果匹配成功,则返回非null值;如果匹配失败或自身取消,则返回null值。TransferStack
,fulfill节点会加入到链表中;队列结构的TransferQueue
,fulfill节点不会加入链表中,而是直接修改交易先到节点的item,这和LinkedTransferQueue一样。