SynchronousQueue是一个特殊的阻塞队列,生产者插入操作后只能等待消费者移除操作,它没有任何容量,甚至没有存储一个元素的容量。在SynchronousQueue中不能使用peek方法,因为元素只有被移除时才存在,只有消费者移除了元素生产者才能往队列中插入元素,当然更不能进行迭代。队列的头节点是第一个排队插入元素的线程,队列是不允许存储null的元素。生产者和消费者必须互相等待,这样经过一个元素的生产到消费的过程,然后一致离开。
SynchronousQueue有两种策略,一种是公平模式,使用队列来完成,一种是非公平模式,使用栈来完成,不论是栈或队列都是用链表来实现的 。看一下SynchronousQueue的创建:
SynchronousQueue<String> s = new SynchronousQueue<>();
SynchronousQueue<String> b = new SynchronousQueue<>(true);
接下来看一下它的构造方法
/**
* 默认无参构造
*/
public SynchronousQueue() {
this(false);
}
/**
* 指定是否使用公平模式
*/
public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
默认是创建一个非公平模式,也可以实现公平模式,如果是公平模式的话,可以保证第一个队首的线程是等待时间最长的线程,这时可以把SynchronousQueue看作是一个FIFO队列。无论是队列或栈,它们都继承了Transferer类。
基本属性
/** CPU的数量 */
static final int NCPUS = Runtime.getRuntime().availableProcessors();
static final int maxTimedSpins = (NCPUS < 2) ? 0 : 32;
static final int maxUntimedSpins = maxTimedSpins * 16;
static final long spinForTimeoutThreshold = 1000L;
这四个属性是用来设置自旋时间的。阻塞是一个非常消耗性能的操作,要进行线程之间上下文的切换,所以一般在阻塞之前进行自旋,不停的使用死循环进行检测,当然不能永远停在循环中,必须要设定时间限定,如果在时间限定内通过自旋完成了某种操作,那就不用阻塞提高了响应速度。如果超时要进行阻塞。
当然在竞争激烈的情况下,自旋的时间长点也是可以的,但是过长的自旋也会白白浪费CPU的时间,所以时间限定并没有一个准确的值,要根据不同的情况进行设定。
首先是获取CPU的数量,分两种情况,一种是设定了时间限的自旋,如果CPU的数量是1,那就不进行自旋,只有一个CPU在进行自旋就不能进行其他操作了,CPU的数量>=2,设定maxTimedSpins为32。另一种是没有设定时间限的自旋,如果CPU的数量>=2,把maxUntimeSpins设为32*16,如果CPU的数量为1,把maxUntimeSpins设为0。
spinForTimeoutThreshold是为了防止自定义的时间过长而设置的,单位是纳秒,如果设定的时间>这个值,那就把spinForTimeoutThreshold当作时间限。
private transient volatile Transferer<E> transferer;
SynchronousQueue内有两个内部类TransferQueue和TransferStack,它们都继承了Transfer类,transferer就是具体实现的引用,所有方法都要基于transferer去执行。
TransferStack
/* Modes for SNodes, ORed together in node fields */
/** 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;
TransferStack的三种状态,REQUEST表示消费者,DATA表示生产者,FULFILLING表示匹配另一个生产者或者消费者。任何线程对TransferStack的操作都应该是这三种状态中的一种。
看一下它的内部类snode。
/** Node class for TransferStacks. */
static final class SNode {
volatile SNode next; // 栈中的下一个节点
volatile SNode match; // 相匹配的节点
volatile Thread waiter; // 当前节点代表的线程
Object item; // 具体的item值或者null
int mode; // 代表当前线程的模式
// item和mode字段不需要设置成volatile类型的,因为它们总是在写入之前,读操作之后
SNode(Object item) { // snode的构造方法
this.item = item;
}
}
// 创建一个snode节点
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类的方法。
casNext就是设置当前节点的下一个节点。
tryCancel就是取消当前操作。
isCancelled就是把当前的match匹配变成this,如果当前节点的match是空说明当前节点的任务还没有完成,换成this匹配自己说明任务被取消。
重点是tryMatch方法。
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;
}
当前节点与s节点进行匹配,如果当前节点的match为null && 利用cas把match设为s,获取当前节点的等待线程,如果等待线程不为null,将当前节点的等待线程重设为null,释放当前的等待线程,直接返回true,表示匹配成功。如果当前节点的match不为null || 利用cas设置失败,判断match==s,如果相等,表示匹配成功,直接返回true,如果不相等,直接返回false。
transfer方法
/**
* 放入或取出item
*/
@SuppressWarnings("unchecked")
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) { // 栈为null 或 栈顶节点的模式与当前元素模式相同
if (timed && nanos <= 0) { // 设置了timed而且等待时间<0,不进行等待
if (h != null && h.isCancelled()) // 栈顶节点不为null && 被取消
casHead(h, h.next); // 利用cas设置头节点的下一个节点变成头节点,就是弹出取消的节点
else // 栈为null 或 栈顶节点没有被取消,直接返回null
return null;
} else if (casHead(h, s = snode(s, e, h, mode))) { // 创建一个snode节点,把该节点设置为头节点
SNode m = awaitFulfill(s, timed, nanos); // 自旋阻塞一直匹配到节点
if (m == s) { // 匹配的节点是s,说明节点被取消了,清除s,直接返回null
clean(s);
return null;
}
if ((h = head) != null && h.next == s) // 新的头节点不为null && 新头节点的next为s,说明有节点插入到s之前
casHead(h, s.next); // 移除h和s节点,设置s的next为头节点
return (E) ((mode == REQUEST) ? m.item : s.item); // 根据mode返回元素
}
} else if (!isFulfilling(h.mode)) { // 如果当前栈顶节点的模式不是Fulfilling,尝试去匹配
if (h.isCancelled()) // 栈顶节点被取消直接设置新的头节点然后进行重试
casHead(h, h.next);
else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) { // 新建一个snode节点把该节点设为头节点
for (;;) { // 自旋直到匹配成功或等待的线程消失
SNode m = s.next; // 保存s的next节点为m,m应该是s的匹配节点
if (m == null) { // 如果m为null,说明其他节点把m匹配了,把头节点设为null
casHead(s, null);
s = null; // 把s置为null以便下次循环时使用一个新的节点
break; // 重新开始主循环
}
SNode mn = m.next; // 保存m的next节点为mn
if (m.tryMatch(s)) { // 如果m和s匹配成功
casHead(s, mn); // 弹出m和s,设置mn为头节点
return (E) ((mode == REQUEST) ? m.item : s.item); // 根据mode返回元素
} else // 如果匹配失败
s.casNext(m, mn); // 弹出m节点
}
}
} else {
SNode m = h.next; // 保存头节点的next节点m
if (m == null) // m为null表示已经被其他节点匹配了,需要弹出头节点
casHead(h, null);
else { // m不为null
SNode mn = m.next; // 保存m的next节点mn
if (m.tryMatch(h)) // 如果m和h匹配成功
casHead(h, mn); // 弹出m和h,设置mn为头节点
else // 如果m和h匹配失败
h.casNext(m, mn); // 弹出m节点
}
}
}
}
这个方法用于生产或消费一个元素,主要分三个步骤。
1. 如果当前栈为空或者栈顶元素的模式与当前元素模式一样:① 判断开启了时间限定模式并且限定的时间的时间已经<=0,如果栈顶节点不为null && 栈顶节点被取消,直接弹出栈顶节点,设置下一个节点为栈顶节点,如果栈为null或到了时间限定后栈顶节点仍然没有被取消,直接返回null。
② 尝试将当前节点设为栈顶节点,通过自旋等待匹配的节点m,如果m=当前节点本身,说明当前节点被取消了,清除节点直接返回null。如果m != 当前节点,说明匹配成功,再次判断一下当前栈顶节点发生了改变,取消当前节点和栈顶节点, 最后返回相匹配的节点。
2. 如果当前栈不为空并且当前节点与栈顶节点的模式相匹配的话:① 如果栈顶节点被取消直接设置下一个节点为栈顶节点。
② 把当前节点给上FULFILLING标记,尝试把当前节点作为头节点,循环直到匹配到相应的节点或等待的节点消失,如果相匹配的节点被其他节点匹配了,要重新寻找头节点继续进行。匹配成功后将这两个节点出栈,返回匹配节点的元素。
3. 如果有节点正在匹配,帮助这个节点完成匹配。如果这个节点的匹配节点为null,说明已经被其它节点匹配过了,直接弹出头节点。如果不为null,匹配成功,弹出这两个节点。继续执行主循环。
awaitFulfill
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 (;;) {
if (w.isInterrupted()) // 当前线程被中断尝试着取消当前节点
s.tryCancel();
SNode m = s.match; // s的匹配节点m
if (m != null) // m不为null,说明存在匹配节点直接返回
return m;
if (timed) { // 开启了时间限定
nanos = deadline - System.nanoTime(); // 计算剩余等待的时间
if (nanos <= 0L) { // nanos<=0, 超时,取消当前节点继续循环
s.tryCancel();
continue;
}
}
if (spins > 0) // 自旋次数>0,每次循环都要自减1
spins = shouldSpin(s) ? (spins-1) : 0;
else if (s.waiter == null) // s的等待线程为null,直接设置当前线程为waiter
s.waiter = w; // establish waiter so can park next iter
else if (!timed) // 没有开启时间限定模式,直接挂起当前线程
LockSupport.park(this);
else if (nanos > spinForTimeoutThreshold) // 如果nanos>1000l,挂起当前线程,否则不挂起线程
LockSupport.parkNanos(this, nanos);
}
}
awaitFulfill会自旋阻塞一直匹配到节点,调用shouldSpin方法来判定是否要进行自旋。
/**
* 如果当前节点是头节点 或者 栈为null 或者 当前节点与栈顶节点模式不匹配
*/
boolean shouldSpin(SNode s) {
SNode h = head;
return (h == s || h == null || isFulfilling(h.mode));
}
在这种条件下,不需要阻塞了,生产者消费者马上到来,这种优化是非常有必要的。在自旋中一直要检测当前线程是否被中断,如果被中断要取消当前线程,取消当前线程就是把当前的引用指向自己,所以返回的匹配节点肯定是自己本身m==s,所以就要调用clean方法清除该节点。
void clean(SNode s) {
s.item = null; // 当前节点的item设为null
s.waiter = null; // 当前节点的waiter设为null
SNode past = s.next; // s的next节点past
if (past != null && past.isCancelled()) // past不为null && past被取消,重新设置past节点
past = past.next;
SNode p;
while ((p = head) != null && p != past && p.isCancelled())// 从栈顶节点开始遍历一直到past节点,将这中间连续被取消的节点移除
casHead(p, p.next);
// 移除上面步骤中没有被移除的非连续的被取消的节点
while (p != null && p != past) {
SNode n = p.next;
if (n != null && n.isCancelled())
p.casNext(n, n.next);
else
p = n;
}
}
可以看出清理被取消的节点,而且还顺带遍历栈,把栈中从栈顶节点到该节点(不包括该节点)被取消的节点全部移除。为什么要用两个while循环那?很是纳闷。
TransferQueue
看一下Qnode类。
static final class QNode {
volatile QNode next; // 队列中的下一个节点
volatile Object item; // 元素值
volatile Thread waiter; // 当前节点线程
final boolean isData; // 当前是否为生产者
// qnode的构造方法
QNode(Object item, boolean isData) {
this.item = item;
this.isData = isData;
}
}
transferqueue的基本属性。
/** 队列的头节点 */
transient volatile QNode head;
/** 队列的尾节点 */
transient volatile QNode tail;
/**
* 一个被取消的节点或许还没有被移除队列,因为当它被取消的时候可能是最后一个被插入的节点
*/
transient volatile QNode cleanMe;
TransferQueue() {
QNode h = new QNode(null, false); // initialize to dummy node.
head = h;
tail = h;
}
transfer方法
/**
* 插入或取出元素
*/
@SuppressWarnings("unchecked")
E transfer(E e, boolean timed, long nanos) {
QNode s = null; // constructed/reused as needed
boolean isData = (e != null); // e==null,说明是取出元素;e!=null,说明是插入元素,即生产者请求将数据插入队列
for (;;) {
QNode t = tail;
QNode h = head;
if (t == null || h == null) // 未初始化的头尾节点
continue; // 继续自旋
if (h == t || t.isData == isData) { // 队列为null 或 尾节点的模式与当前节点的模式相同
QNode tn = t.next; // 保存尾节点的next节点为tn
if (t != tail) // t不是尾节点,重试
continue;
if (tn != null) { // tn不为null,说明有其它线程已经抢占添加了tn节点
advanceTail(t, tn); // 如果t是尾节点利用cas把tn设为尾节点
continue; // 继续重试
}
if (timed && nanos <= 0) // 如果设定了时间限定并且时间已到,不在进行等待,直接返回null
return null;
if (s == null) // 新建一个qnode节点,赋给s
s = new QNode(e, isData);
if (!t.casNext(null, s)) // 如果不能使用cas把t的next设为s,重试
continue;
advanceTail(t, s); // 重设尾节点
Object x = awaitFulfill(s, e, timed, nanos); // 自旋阻塞直到匹配到s节点
if (x == s) { // s的匹配节点是自己本身,说明节点被取消,清除s,直接返回null
clean(t, s);
return null;
}
if (!s.isOffList()) { // s节点还没有离开队列
advanceHead(t, s); // 将s设置为头节点
if (x != null)
s.item = s;
s.waiter = null; // 方法s节点的线程
}
return (x != null) ? (E)x : e;
} else { // 互补模式
QNode m = h.next;
if (t != tail || m == null || h != head) // t不是尾节点 或 m为null 或 h不是头节点,读不一致,重试
continue;
Object x = m.item;
if (isData == (x != null) || // 判定x的模式是否与isdata相同,相同说明m早已经被匹配过了
x == m || // m被取消
!m.casItem(x, e)) { // cas失败
advanceHead(h, m); // 将m设为头节点出列重试
continue;
}
advanceHead(h, m); // 匹配成功将m设为头节点
LockSupport.unpark(m.waiter); // 释放m的等待线程
return (x != null) ? (E)x : e;
}
}
}
transfer方法分三种情况,步骤如下:
2. 队列为空或当前节点的模式与尾节点模式相同,将节点加入到等待队列中,直到匹配的节点被取消或者超时,匹配成功,如果返回的节点本身说明node被取消或超时,直接返回null,否则返回真正的值。
4. 如果有节点正在匹配,帮助这个节点完成匹配。
如果队列为空,节点入队,调用awaitFulfill自旋,直到获取到匹配的节点或节点被取消超时。
Object awaitFulfill(QNode s, E e, boolean timed, long nanos) {
/* 如果设定了时间限定计算超时时间 */
final long deadline = timed ? System.nanoTime() + nanos : 0L;
Thread w = Thread.currentThread(); // 获取当前线程
int spins = ((head.next == s) ? // 自旋次数,如果这个节点的前节点正好是head节点,那就进行自旋操作,主要是避免减少线程之间的上下文切换
(timed ? maxTimedSpins : maxUntimedSpins) : 0);
for (;;) {
if (w.isInterrupted()) // 线程被中断过直接移除该节点
s.tryCancel(e);
Object x = s.item;
if (x != e) // 线程被中断 或 进行了阻塞唤醒 或 超时,那么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为null设置当前线程为它的waiter
s.waiter = w;
else if (!timed) // 没有开启时间限定直接挂起当前线程进行阻塞
LockSupport.park(this);
else if (nanos > spinForTimeoutThreshold) // 设定的时间>1000l,在指定时间内挂起当前线程
LockSupport.parkNanos(this, nanos);
}
}
head节点永远是个空节点,所以先判断当前节点是否的head的next节点,这样就先进行自旋,主要还是为了生产者或消费者来了就能立刻匹配,如果挂起再进行唤醒太消耗性能。再自旋中达到一定次数就挂起线程,不能无限自旋,以免浪费cpu的时间。
如果返回的节点是自己本身,所以x==s,就要调用clean方法取消该节点。
void clean(QNode pred, QNode s) {
s.waiter = null; // 把s节点的线程设为null
while (pred.next == s) { // Return early if already unlinked
QNode h = head;
QNode hn = h.next;
if (hn != null && hn.isCancelled()) { // hn不为null && hn被取消,重新头节点为hn,继续重试
advanceHead(h, hn);
continue;
}
QNode t = tail; // 获取尾节点,确保对尾节点读的一致性
if (t == h) // 队列为空直接返回
return;
QNode tn = t.next; // 保存尾节点的next节点
if (t != tail) //
continue;
if (tn != null) {
advanceTail(t, tn);
continue;
}
if (s != t) { // s不是尾节点,如果s被取消 或 断开s直接退出
QNode sn = s.next;
if (sn == s || pred.casNext(s, sn))
return;
}
QNode dp = cleanMe; // 这种情况出现删除的只能是队尾节点
if (dp != null) { // 如果dp不为null,说明了
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) // 如果dp==pred,说明清除s成功,直接返回
return; // s is already saved node
} else if (casCleanMe(null, pred)) // cleanMe是null,所以使用cas把pred设为cleanMe,目的就是为了清除s做准备
return;
}
}
这个clean方法没有完全读懂,感觉好有难度。引用一下别人的分析。