原来以为TransferQueue和TransferStack的实现应该是一样的,一个是Queue,一个是Stack,take的时候都是从head获取,只是put的时候不一样,Queue是放在队尾,Stack是压栈放在栈首。所以想两者的区别只是put不一样就可以了。
但是实际他们的算法差距不止是在put,差距还是比较大的,理解了TransferStack的源码,并不是理所当然直接就能轻松理解TransferQueue的。
TransferQueue#QNode
与TransferStack类似,通过QNode(TransferStack的叫SNode)存储“数据”或者获取数据的“请求”。
QNode主要属性:
static final class QNode {
volatile QNode next; // next node in queue
volatile Object item; // CAS'ed to or from null
volatile Thread waiter; // to control park/unpark
final boolean isData;
next:下一节点。
waiter:该节点绑定的线程,与SNode的waiter含义一样。
item:与SNode的item含义一致。
isData:类似于SNode的mode,true表示是数据,false表示该节点是request。
head:头节点,或者叫首节点。
tail:尾结点。
比较讨厌的是,他没有match!他也没有FULFILLING的mode,所以也就暗示了他的算法逻辑和TransferStack的差距可能会比较大。
QNode提供了几个原子性的操作:
- casNext:cas方式替换当前节点的下一节点
- casItem:cas方式替换当前节点的item
- tryCancel;cas方式设置当前节点的item为自己,用这种方式表示该节点被calcel
- advanceHead:cas的方式修改头节点,其实就是队列从head出队列
- advanceTail:从队尾入队
TransferQueue#初始化
TransferQueue() {
QNode h = new QNode(null, false); // initialize to dummy node.
head = h;
tail = h;
}
构造函数创建了一个“dummy node”并设置为head、tail,需要注意,这个“dummy node”在take操作的时候必须要跳过。
TransferQueue#transfer
TransferQueue的重头戏,入队列和出队列都是这一个方法。
代码的解析放在注释中了:
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
//队列空,或者当前操作与tail的mode相同
//这种情况下,队列内的节点与当前请求节点无法匹配,所以,当前请求节点入队
if (h == t || t.isData == isData) { // empty or same-mode
QNode tn = t.next;
//并发操作过程中,t已经不是尾结点了(其他线程有节点入队了),重来主循环
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);
//等待匹配过程中,节点取消,则clean之后,返回null
if (x == s) { // wait was cancelled
clean(t, s);
return null;
}
//完成匹配了,要么就是自旋过程中直接完成了匹配,要么就是阻塞后被匹配成功的节点唤醒了
//如果s还在队列中
if (!s.isOffList()) { // not already unlinked
//则s的上一节点如果是首节点的话,换成s为首节点,原来的首节点t移除队列
//接下来的操作逻辑基于:s已经匹配成功,原来的头节点出队列,s如果是数据节点的话解除s对数据的引用,s节点变为一个dummy类的节点变更为head
advanceHead(t, s); // unlink if head
//匹配成功后,如果x!=null,说明当前节点s是"request",匹配到了“data”
if (x != null) // and forget fields
//s的item指向自己,因为x就要返回来,释放掉s对x的引用
s.item = s;
//s的等待线程置空
s.waiter = null;
}
//匹配成功,返回
return (x != null) ? (E)x : e;
//否则,队列不空并且mode不同,可以匹配
} else { // complementary-mode
//准备进行匹配,注意获取的是首节点的下一个节点,跳过了首节点
QNode m = h.next; // node to fulfill
//处理过程中队列发生了变化,重来
if (t != tail || m == null || h != head)
continue; // inconsistent read
Object x = m.item;
//如果m已经被其他节点匹配过,或者m被取消,或者匹配失败,就把m更换为头节点,继续主循环
//如果m没有被匹配也没有被取消,则通过casItem的方式进行匹配,匹配完成后,m节点的isData会变更为反向
//即:request节点变更为data,data节点变更为request,
//这个操作是为了在唤醒对应的阻塞进程后使得阻塞进程满足完成匹配的条件,类似于Stack的match赋值
if (isData == (x != null) || // m already fulfilled
x == m || // m cancelled
!m.casItem(x, e)) { // lost CAS
advanceHead(h, m); // dequeue and retry
continue;
}
//匹配完成的话,首节点出队列,需要注意,m节点并没有出队列,而是变为了首节点,
//阻塞进程被唤醒后会把m节点处理为类似于dummy节点。首节点在下次匹配的时候会被跳过
advanceHead(h, m); // successfully fulfilled
//唤醒m节点的阻塞进程
LockSupport.unpark(m.waiter);
//返回
return (x != null) ? (E)x : e;
}
}
}
TransferQueue#awaitFulfill
awaitFulfill的作用是通过自旋、或者阻塞当前线程来等待节点被匹配。
参数变成4个了:
QNode s:等待匹配的节点。
E e:匹配判断标准,和TransferStack的完全不一样了,TransferStack是通过match属性来判断的,TransferQueue是用了这么个玩意儿,很不容易理解,初始送进来的e就是s的item,稍后解释具体匹配规则。
booean timed:true则表示限时等待。
long nanos:限时等待时长。
Object awaitFulfill(QNode s, E e, boolean timed, long nanos) {
//自旋时长、timeout时长等
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;
//刚开始e==x,用x!=e来判断是否匹配的话,肯定是没有完成匹配
//在自旋或线程阻塞等待的过程中,变化的是x,也就是队列中s的item在其他线程完成匹配后会被更换
//具体参考transfer方法中的匹配部分的casItem,就是干这个的,交换完成之后唤醒本进程,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);
}
}
代码注释中解释过了,刚开始调用的时候e==x,用x!=e来判断是否匹配的话,肯定是没有完成匹配。
在自旋或线程阻塞等待的过程中,变化的是x,也就是队列中s的item在其他线程完成匹配后会被更换,具体参考transfer方法中的匹配部分的casItem,就是干这个的,交换完成之后唤醒本进程,x!=e就成立了,匹配得以完成。
TransferQueue#clean
和TransferStack的clean方法功能一样,就是要清理掉尚未完成匹配就被取消掉的节点,检查当前节点如果不是尾结点的话就清出队列。
小结
SynchronousQueue的源码就分析完成了,虽然代码量不大,但却不太容易理解。需要反复阅读、对照写入队列、从队列获取数据两种类型的操作、设想多线程并发情况下的操作场景、在空白纸上画出队列在各场景、各给定时间点的状态,有助于更好的理解代码逻辑。
Thanks a lot!
上一篇 BlockQueue - 基于TransferStack的SynchronousQueue
下一篇 [BlockingQueue - LinkedBlockingQueue]{https://segmentfault.com/a/1190000043516074}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。