SynchronousQueue是一个不存储元素的队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。
它支持公平访问队列。默认情况下线程采用非公平性策略访问队列。SynchronousQueue类只有两个构造方法:
public SynchronousQueue() {
this(false);
}
public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue() : new TransferStack();
}
使用第二个构造方法可以创建公平性访问的SynchronousQueue,如果设置为true,则等待的线程会采用先进先出的顺序访问队列。
SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合传递性场景。SynchronousQueue的吞吐量高于ArrayBlockingQueue和LinkedBlockingQueue。
SynchronousQueue类的定义如下:
public class SynchronousQueue extends AbstractQueue implements BlockingQueue, java.io.Serializable
该类同样继承自AbstractQueue抽象类,并实现了BlockingQueue接口,这里不再叙述。
SynchronousQueue类使用了一个非常关键的内部类来转移数据:
abstract static class Transferer {
/**
* Performs a put or take.
*
* @param e if non-null, the item to be handed to a consumer;
* if null, requests that transfer return an item
* offered by producer.
* @param timed if this operation should timeout
* @param nanos the timeout, in nanoseconds
* @return if non-null, the item provided or received; if null,
* the operation failed due to timeout or interrupt --
* the caller can distinguish which of these occurred
* by checking Thread.interrupted.
*/
abstract E transfer(E e, boolean timed, long nanos);
}
从注释中可以看出,该类的唯一一个transfer方法是通过参数e来区分调用方法的是一个生产者线程还是一个消费者线程,如果e为null,则说明这是一个消费者线程,比如一个take操作,如果e不为null,那么就是一个生产者线程,这个数据就是这个线程需要交付的数据,比如一个put操作。
SynchronousQueue采用队列TransferQueue来实现公平性策略,采用堆栈TransferStack来实现非公平性策略,SynchronousQueue的put、take操作都是委托这两个类来实现的,我们下面先来了解一下这两个类。
TransferQueue继承自Transferer:
static final class TransferQueue extends Transferer
它使用队列作为交易媒介,来实现公平交易,TransferQueue使用QNode类来作为队列节点:
static final class QNode {
// 指向下一个节点
volatile QNode next; // next node in queue
// item数据项
volatile Object item; // CAS'ed to or from null
// 等待线程
volatile Thread waiter; // to control park/unpark
// 是否为数据的标识
final boolean isData;
...
}
TransferQueue类中主要有3个QNode的对象:
/** Head of queue */
transient volatile QNode head;
/** Tail of queue */
transient volatile QNode tail;
/**
* Reference to a cancelled node that might not yet have been
* unlinked from queue because it was the last inserted node
* when it was cancelled.
*/
transient volatile QNode cleanMe;
同时,对于TransferQueue需要注意的是,其队列永远都存在一个dummy node,在构造时创建:
TransferQueue() {
QNode h = new QNode(null, false); // initialize to dummy node.
head = h;
tail = h;
}
TransferStack同样继承自Transferer:
static final class TransferStack extends Transfer
它使用栈作为交易媒介,来实现非公平交易,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
// item数据线
Object item; // data; or null for REQUESTs
// 节点状态
int mode;
...
}
节点主要有以下几种状态:
/** Node represents an unfulfilled consumer */
static final int REQUEST = 0;
/** Node represents an unfulfilled producer */
static final int DATA = 1;
/** Node is fulfilling another unfulfilled DATA or REQUEST */
static final int FULFILLING = 2;
SynchronousQueue的put、take操作都是调用TransferQueue或者TransferStack的transfer方法来实现的,我们先来看一下这两个方法:
public void put(E e) throws InterruptedException {
// 若插入的数据是null,则直接抛出NullPointerException异常
if (e == null) throw new NullPointerException();
// 调用transfer方法
if (transferer.transfer(e, false, 0) == null) {
Thread.interrupted();
throw new InterruptedException();
}
}
public E take() throws InterruptedException {
// 调用transfer方法
E e = transferer.transfer(null, false, 0);
// 若值不为null,则直接返回
if (e != null)
return e;
Thread.interrupted();
throw new InterruptedException();
}
从源码中可以看到,这两个方法都会调用transfer方法,其中,put方法传递的是e参数,所以模式为数据(公平isData = true,非公平mode= DATA),而take方法传递的是null,所以模式为请求(公平isData = false,非公平mode = REQUEST)。我们下面看一看在公平与非公平模式下的transfer方法具体实现。
TransferQueue实现的transfer方法如下:
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
// 头尾节点相等(队列为null),或者当前节点和队列尾节点具有相同的交易类型
// 将节点添加到队列尾部,并且等待匹配
if (h == t || t.isData == isData) { // empty or same-mode
QNode tn = t.next;
// t != tail表明已有其他线程修改了tail,当前线程需要重新再来
if (t != tail) // inconsistent read
continue;
// 若尾节点的后继节点不为null,则表明已经有其他线程添加了节点,更新尾节点
if (tn != null) { // lagging tail
advanceTail(t, tn);
continue;
}
// 超时
if (timed && nanos <= 0) // can't wait
return null;
// s == 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
// 调用awaitFulfill方法,若节点是head.next,则进行自旋
// 否则,直接阻塞,直到有其他线程与之匹配,或它自己进行线程的中断
Object x = awaitFulfill(s, e, timed, nanos);
// 若返回的x == s表示,当前线程已经超时或者中断,不然的话s == null或者是匹配的节点
if (x == s) { // wait was cancelled
clean(t, s);
return null;
}
// 若s节点还没有从队列删除
if (!s.isOffList()) { // not already unlinked
// 尝试将s节点设置为head,移出t
advanceHead(t, s); // unlink if head
if (x != null) // and forget fields
s.item = s;
s.waiter = null;
}
return (x != null) ? (E)x : e;
}
// 这里是从head.next开始,因为TransferQueue总是会存在一个dummy节点
else { // complementary-mode
QNode m = h.next; // node to fulfill
// 不一致读,表明有其他线程修改了队列
if (t != tail || m == null || h != head)
continue; // inconsistent read
Object x = m.item;
// isData == (x != null):判断isData与x的模式是否相同,相同表示已经匹配了
// x == m :m节点被取消了
// !m.casItem(x, e):如果尝试将数据e设置到m上失败
if (isData == (x != null) || // m already fulfilled
x == m || // m cancelled
!m.casItem(x, e)) { // lost CAS
// 将m设置为头结点,h出列,然后重试
advanceHead(h, m); // dequeue and retry
continue;
}
// 成功匹配了,m设置为头结点h出列,向前推进
advanceHead(h, m); // successfully fulfilled
// 唤醒m的等待线程
LockSupport.unpark(m.waiter);
return (x != null) ? (E)x : e;
}
}
}
该方法的主要运行过程如下:
1、如果队列为空,或者请求交易的节点和队列中的节点具有相同的交易类型,那么就将该请求交易的节点添加到队列尾部等待交易,直到被匹配或者被取消。
2、如果队列中包含了等待的节点,并且请求的节点和等待的节点是互补的,那么进行匹配并且进行交易。
当队列为空时,节点入列然后通过调用awaitFulfill()方法自旋,该方法主要用于自旋/阻塞节点,直到节点被匹配返回或者取消、中断:
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);
// 如果线程进行了阻塞 -> 唤醒或者中断了,那么x != e 肯定成立,直接返回当前节点即可
Object x = s.item;
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);
}
}
在自旋/阻塞过程中做了一点优化,就是判断当前节点是否为对头元素,如果是的则先自旋,如果自旋次数过了,则才阻塞,这样做的主要目的就在如果生产者、消费者立马来匹配了则不需要阻塞,因为阻塞、唤醒会消耗资源。在整个自旋的过程中会不断判断是否超时或者中断了,如果中断或者超时了则调用tryCancel()取消该节点。
void tryCancel(Object cmp) {
UNSAFE.compareAndSwapObject(this, itemOffset, cmp, this);
}
取消过程就是将节点的item设置为自身(itemOffset是item的偏移量)。所以在调用awaitFulfill()方法时,如果当前线程被取消、中断、超时了那么返回的值肯定是s,否则返回的则是匹配的节点。如果返回值是节点s,那么if(x == s)必定成立,如下:
if (x == s) { // wait was cancelled
clean(t, s);
return null;
}
如果返回的x == s成立,则调用clean()方法清理节点s:
void clean(QNode pred, QNode s) {
s.waiter = null; // forget thread
/*
* At any given time, exactly one node on list cannot be
* deleted -- the last inserted node. To accommodate this,
* if we cannot delete s, we save its predecessor as
* "cleanMe", deleting the previously saved version
* first. At least one of node s or the node previously
* saved can always be deleted, so this always terminates.
*/
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;
if (sn == s || pred.casNext(s, sn))
return;
}
QNode dp = cleanMe;
if (dp != null) { // Try unlinking previous cancelled node
QNode d = dp.next;
QNode dn;
if (d == null || // d is gone or
d == dp || // d is off list or
!d.isCancelled() || // d not cancelled or
(d != t && // d not tail and
(dn = d.next) != null && // has successor
dn != d && // that is on list
dp.casNext(d, dn))) // d unspliced
casCleanMe(dp, null);
if (dp == pred)
return; // s is already saved node
} else if (casCleanMe(null, pred))
return; // Postpone cleaning s
}
}
我们看方法中的注释:
不论任何情况,列表上最后插入的节点不能被删除。为了适应这一点,如果我们不能删除s,我们将其前驱设置为“CleanMe”,先删除以前保存的版本。节点s或先前保存的节点中的至少一个总是可以被删除。
该方法的主要逻辑如下:
1、删除的节点不是queue尾节点, 这时直接以pred.casNext(s, s.next)方式来进行删除
2、删除的节点是队尾节点:
TransferStack实现的transfer方法如下:
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;
// 栈为空或者当前节点模式与头节点模式一样,将节点压入栈内,等待匹配
if (h == null || h.mode == mode) { // empty or same-mode
// 超时
if (timed && nanos <= 0) { // can't wait
// 节点被取消了,弹出被取消的节点
if (h != null && h.isCancelled())
casHead(h, h.next); // pop cancelled node
else
return null;
}
// 未超时,创建SNode节点
else if (casHead(h, s = snode(s, e, h, mode))) {
// 自旋,等待匹配
SNode m = awaitFulfill(s, timed, nanos);
// 返回的m == s 表示该节点被取消了或者超时、中断了
if (m == s) { // wait was cancelled
// 清理节点s,返回null
clean(s);
return null;
}
// 因为通过前面一步将s替换成了head,如果h.next == s,则表示有其他节点插入到s前面了,变成了head
// 且该节点就是与节点s匹配的节点
if ((h = head) != null && h.next == s)
casHead(h, s.next); // help s's fulfiller
// 如果是请求则返回匹配的域,否则返回节点s的域
return (E) ((mode == REQUEST) ? m.item : s.item);
}
}
// 如果栈不为null,且两者模式不匹配(h != null && h.mode != mode)
// 说明他们是一队对等匹配的节点,尝试用当前节点s来满足h节点
else if (!isFulfilling(h.mode)) { // try to fulfill
// head 节点已经取消了,向前推进
if (h.isCancelled()) // already cancelled
casHead(h, h.next); // pop and retry
// 尝试将当前节点打上“正在匹配”的标记,并设置为head
else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
for (;;) { // loop until matched or waiters disappear
// s为当前节点,m是s的next节点,
// m节点是s节点的匹配节点
SNode m = s.next; // m is s's match
// m == null,其他节点把m节点匹配走了
if (m == null) { // all waiters are gone
// 将s弹出
casHead(s, null); // pop fulfill node
// 将s置空,下轮循环的时候还会新建
s = null; // use new node next time
break; // restart main loop
}
// 获取m的next节点
SNode mn = m.next;
// 尝试匹配
if (m.tryMatch(s)) {
// 匹配成功,将s、m弹出
casHead(s, mn); // pop both s and m
return (E) ((mode == REQUEST) ? m.item : s.item);
} else // lost match
// 如果没有匹配成功,说明有其他线程已经匹配了,把m移出
s.casNext(m, mn); // help unlink
}
}
}
// 到这最后一步说明节点正在匹配阶段
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 {
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
}
}
}
}
该方法的主要逻辑如下:
1、如果当前的交易栈是空的,或者包含与请求交易节点模式相同的节点,那么就将这个请求交易的节点作为新的栈顶节点,等待被下一个请求交易的节点匹配,最后会返回匹配节点的数据或者null,如果被取消则会返回null。
2、如果当前交易栈不为空,并且请求交易的节点和当前栈顶节点模式互补,那么将这个请求交易的节点的模式变为FULFILLING,然后将其压入栈中,和互补的节点进行匹配,完成交易之后将两个节点一起弹出,并且返回交易的数据。
3、如果栈顶已经存在一个模式为FULFILLING的节点,说明栈顶的节点正在进行匹配,那么就帮助这个栈顶节点快速完成交易,然后继续交易。
当节点加入栈内后,通过调用awaitFulfill()方法自旋等待节点匹配:
SNode awaitFulfill(SNode s, boolean timed, long nanos) {
// 计算超时时间点
final long deadline = timed ? System.nanoTime() + nanos : 0L;
// 获取当前线程对象
Thread w = Thread.currentThread();
// 自旋次数
// shouldSpin 用于检测当前节点是否需要自旋
// 如果栈为空、该节点是首节点或者该节点是匹配节点,则先采用自旋,否则阻塞
int spins = (shouldSpin(s) ?
(timed ? maxTimedSpins : maxUntimedSpins) : 0);
for (;;) {
// 线程中断了,取消该节点
if (w.isInterrupted())
s.tryCancel();
// 匹配节点
SNode m = s.match;
// 如果匹配节点m不为空,则表示匹配成功,直接返回
if (m != null)
return m;
// 超时
if (timed) {
nanos = deadline - System.nanoTime();
// 节点超时,取消
if (nanos <= 0L) {
s.tryCancel();
continue;
}
}
// 每次自旋的时候都需要检查自身是否满足自旋条件,满足就 - 1,否则为0
if (spins > 0)
spins = shouldSpin(s) ? (spins-1) : 0;
// 第一次阻塞时,会将当前线程设置到s上
else if (s.waiter == null)
s.waiter = w;
// 阻塞当前线程
else if (!timed)
LockSupport.park(this);
// 超时
else if (nanos > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanos);
}
}
这个线程一直阻塞直到被匹配,在阻塞之前首先会自旋,这个自旋会在阻塞之前进行,它会调用shouldSpin方法来进行判断是否需要自旋,下面展示了shouldSpin这个方法:
boolean shouldSpin(SNode s) {
SNode h = head;
return (h == s || h == null || isFulfilling(h.mode));
}
如果当前节点在栈顶,并且正在请求交易,那么就应该自旋。在多CPU的环境下,这种情况下的自旋是有必要的,因为很可能立刻就会有新的线程到来,那么就会立刻进行交易而不需要进行阻塞,然后被唤醒,这是需要过程的,所以这样的自旋等待是值得的。
若线程被中断了,则调用tryCancel()方法取消该节点:
void tryCancel() {
UNSAFE.compareAndSwapObject(this, matchOffset, null, this);
}
该过程和TransferQueue相同。awaitFullfill()方法如果返回的m == s,则表示当前节点已经中断取消了,则需要调用clean()方法,清理节点s:
void clean(SNode s) {
s.item = null; // forget item
s.waiter = null; // forget thread
SNode past = s.next;
if (past != null && past.isCancelled())
past = past.next;
// Absorb cancelled nodes at head
SNode p;
while ((p = head) != null && p != past && p.isCancelled())
casHead(p, p.next);
// Unsplice embedded nodes
while (p != null && p != past) {
SNode n = p.next;
if (n != null && n.isCancelled())
p.casNext(n, n.next);
else
p = n;
}
}
clean()方法就是将head节点到s节点之间所有已经取消的节点全部移出。
SynchronousQueue的实现有点难,有些看不懂啊。。。
【死磕Java并发】-----J.U.C之阻塞队列:SynchronousQueue
Java阻塞队列SynchronousQueue详解