挖掘队列源码之SynchronousQueue
前言
探索SynchronousQueue是基于JDK1.8,属于特殊的阻塞队列,内部并无容量,是典型的生产者/消费者模式,只含有生产者或消费者的场景下会发生阻塞,当既有生产者也有消费者下会发生匹配从而完成交易。SynchronousQueue有一个fair属性用于配置使用哪种内部类,fair为true情况下使用先进先出的队列,称为公平策略;fair为false情况下使用先进后出的栈,称为非公平策略,除了在线程池遇到过该类,工作上使用的频率很低。
数据结构
public class SynchronousQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {
// 设置等待时间时的自旋次数,等待节点匹配
static final int maxTimedSpins = (NCPUS < 2) ? 0 : 32;
// 未设置等待时间时的自旋次数,该数值通常会比maxTimedSpins大,因为此种情况下不需要检验时间
static final int maxUntimedSpins = maxTimedSpins * 16;
// 设置等待时间的情况下并不是直接利用LockSupport.parkNanos等待指定时间,实际上在这等待时间内有可能早就匹配了,所以它采用了优先自旋,等自旋的次数到达之后在等待剩余的时间,这种方式要比直接等待的方式更好
static final long spinForTimeoutThreshold = 1000L;
// 通过不同的媒介来传递节点信息,共有两种媒介,一个是队列,其特性是先进先出,一个是栈,其特性是先进后出
private transient volatile Transferer<E> transferer;
}
构造函数
/**
* 初始化,采用非公平策略
*/
public SynchronousQueue() {
this(false);
}
/**
* 初始化
* @param fair true:公平策略 false:非公平策略
*/
public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
简单方法
// 通过栈结构作为媒介来传递节点信息,栈结构的特性是先进后出,意思是先入栈的节点会后面在匹配
static final class TransferStack<E> extends Transferer<E> {
// 栈顶节点
volatile SNode head;
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;
}
/**
* 更新栈顶节点
* @param h 原栈顶节点
* @param nh 新栈顶节点
* @return 结果值
*/
boolean casHead(SNode h, SNode nh) {
return h == head && UNSAFE.compareAndSwapObject(this, headOffset, h, nh);
}
// 节点信息
static final class SNode {
// 下一个节点
volatile SNode next;
// 匹配的节点,要么因为被取消指向自身,要么是匹配成功指向其他节点,要么是还未匹配
volatile SNode match;
// 控制park/unpark
volatile Thread waiter;
// 当前节点的数据
Object item;
// put/take模式
int mode;
/**
* 初始化
* @param item 节点的值
*/
SNode(Object item) {
this.item = item;
}
/**
* 取消当前节点
*/
void tryCancel() {
UNSAFE.compareAndSwapObject(this, matchOffset, null, this);
}
/**
* 更新当前节点的下一个节点为指定节点
* @param cmp 旧下一个节点
* @param val 新下一个节点
* @return 结果值
*/
boolean casNext(SNode cmp, SNode val) {
return cmp == next && UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
/**
* 判断当前节点是否与指定节点相匹配
* @param s 指定节点
* @return 结果值
*/
boolean tryMatch(SNode s) {
// match 只能是节点被取消后指向自身或是匹配成功后指向指定节点或还未匹配指向null
if (match == null && UNSAFE.compareAndSwapObject(this, matchOffset, null, s)) {
Thread w = waiter;
if (w != null) { // 唤醒阻塞线程
waiter = null;
LockSupport.unpark(w);
}
return true;
}
return match == s;
}
}
/**
* 生产者添加节点,消费者获取生产者生产的产品,所以put/take都会调用此方法
* 若是匹配成功,当前节点是生产者则最终返回自身的数据,当前节点是消费者则最终返回生产者的数据(产品)
* 若是当前节点被取消了,则返回null
* @param e 当前节点的值
* @param timed 是否指定了阻塞时间
* @param nanos 阻塞时间
* @return 结果值,参考注释
*/
E transfer(E e, boolean timed, long nanos) {
SNode s = null;
int mode = (e == null) ? REQUEST : DATA; // put/take都会调用此方法,当然了,这两个方法的目的肯定是不同的,所以导致了两种模式,REQUEST -> take (消费者),DATA -> put (生产者),FULFILLING|mode (匹配中)
for (;;) {
SNode h = head;
/**
* 1. 栈顶节点未初始化,也就是说此时没有生产者也没有消费者,因为put/take都有可能调用此方法,不管是哪个调用都会作为栈顶节点,只是模式不同
* 2. 已存在栈顶节点了,这个时又添加了新的节点,那么就要已存在的栈顶节点与新节点是否能够匹配,即能够组成一对,有生产者也要有消费者,只有这样子的情况下才有可能匹配成功
* 如果两者的模式是一样的,那就压根不用判断了,直接入栈
*/
if (h == null || h.mode == mode) {
if (timed && nanos <= 0) { // 进入到这里说明栈中还未能有一对,在不等待的前提下最终只能是返回null
if (h != null && h.isCancelled()) // 顺带清理下已取消的节点
casHead(h, h.next);
else
return null;
} else if (casHead(h, s = snode(s, e, h, mode))) { // 更新栈顶节点为s,新栈顶节点的下一个节点是原栈顶节点
SNode m = awaitFulfill(s, timed, nanos);
if (m == s) { // awaitFullfill只可能返回已匹配的节点或自身节点,若是自身节点说明被取消了
clean(s);
return null;
}
if ((h = head) != null && h.next == s) // 此种情况说明新栈顶节点与旧栈顶节点已匹配成功
casHead(h, s.next);
return (E) ((mode == REQUEST) ? m.item : s.item); // 若当前节点是消费者自然就能获取生产者的数据,若是生产者自然就只能返回自身的数据了
}
} else if (!isFulfilling(h.mode)) { // 栈顶节点未匹配,此时栈顶节点有可能是消费者,当前节点是生产者,或栈顶节点是生产者,当前节点是消费者,总之这两个节点就是一次交易,有生产有消费,可以完成匹配
if (h.isCancelled()) // 栈顶节点被取消自然就换下一个节点了
casHead(h, h.next);
else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) { // 更新栈顶节点,同时将其模式改成匹配中
for (;;) {
SNode m = s.next; // m 与 s 进行匹配
if (m == null) { // m == null 说明栈中就只有一个节点,其他节点有可能因为取消而被清除了,此时无论在怎么匹配都没有办法组成一对了,所以只好跳出循环重新阻塞等待
casHead(s, null); // 移除栈顶节点,当前节点并不会丢失,跳出循环后自然会在入栈重新阻塞等待
s = null;
break;
}
SNode mn = m.next;
if (m.tryMatch(s)) {
casHead(s, mn); // 匹配成功后应该是一对完成的交易,即有生产者也有消费者,所以这两个节点都会被移除,直接更新栈顶节点了
return (E) ((mode == REQUEST) ? m.item : s.item); // 若当前节点是消费者自然就能获取生产者的数据,若是生产者自然就只能返回自身的数据了
} else
s.casNext(m, mn); // 走到这里说明m节点被取消了,关联其下一个节点
}
}
} else { // 走到这里说明栈顶节点正在匹配中,帮助匹配...
SNode m = h.next; // m 与 h 进行匹配
if (m == null)
casHead(h, null);
else {
SNode mn = m.next;
if (m.tryMatch(h))
casHead(h, mn);
else
h.casNext(m, mn);
}
}
}
}
/**
* 通过自旋 + 阻塞的方式等待当前节点被匹配
* @param s 当前节点
* @param timed 是否指定了阻塞时间
* @param nanos 阻塞时间
* @return 已取消指向自身,匹配成功指向其他节点
*/
SNode awaitFulfill(SNode s, boolean timed, long nanos) {
final long deadline = timed ? System.nanoTime() + nanos : 0L;
Thread w = Thread.currentThread();
/**
* 如果当前节点不是栈顶节点的话则不自旋,因为还不是它的轮次,还没到资格,即使自旋了也是白白浪费CPU资源
* 或者说栈顶节点正在匹配中,此时能调用awaitFulfill方法说明当前节点原本是栈顶节点,但因为其他线程的调用导致栈顶节点更新
* 不过这会使得新栈顶节点的下一个节点就是旧栈顶节点,也就是当前节点,简单来说,新栈顶节点与旧栈顶节点(当前节点)将会决定是否能够匹配
* 所以如果栈顶节点更新了,说明添加了新的节点,如果这个时候它正好处于匹配中,那么就可以决定是否能够匹配,如果不处于匹配中,说明它们两者都属于同一种模式,要么都是
* 生产者,要么都是消费者,这无法组成一对,所以用不着自旋了,不知道讲明白没...
*/
int spins = (shouldSpin(s) ? (timed ? maxTimedSpins : maxUntimedSpins) : 0);
for (;;) {
if (w.isInterrupted())
s.tryCancel(); // 线程被中断取消当前节点,实际上就是让自己匹配自身 s.match = this
SNode m = s.match;
if (m != null) // 当前节点被匹配/取消,若节点被匹配了,那么m指向其他节点,若节点被取消了,那么m指向自身
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)
s.waiter = w; // 赋值,当匹配后则对其进行唤醒
else if (!timed)
LockSupport.park(this);
else if (nanos > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanos);
}
}
/**
* 清除栈顶节点到指定节点之间已取消的节点
* @param s 指定节点
*/
void clean(SNode s) {
s.item = null;
s.waiter = null;
SNode past = s.next;
if (past != null && past.isCancelled())
past = past.next;
/**
* 从栈顶节点开始清除,若栈顶节点被取消了则将其下一个节点更新为栈顶节点继续判断,直到遇到栈顶节点未取消或s.next节点
* 有可能出现s.next节点已取消且还是栈顶节点,注释上说它们并未对这种情况做检验是不想在遍历,实际上即使出现这种情况在代码中还是有判断栈顶阶段是否已取消
*/
SNode p;
while ((p = head) != null && p != past && p.isCancelled())
casHead(p, p.next);
/**
* 如果此时p != past,那么p节点与past节点之间仍然存在已取消的节点,所以这里同样做了清除直到遇到past就停下来了
*/
while (p != null && p != past) {
SNode n = p.next;
if (n != null && n.isCancelled())
p.casNext(n, n.next); // 更新下一个节点
else
p = n;
}
}
/**
* 当前节点是否要自旋
* @param s 当前节点
* @return 结果值
*/
boolean shouldSpin(SNode s) {
SNode h = head;
return (h == s || h == null || isFulfilling(h.mode));
}
/**
* 栈顶节点是否正在匹配中
* @param m 栈顶节点的模式
* @return 结果值
*/
static boolean isFulfilling(int m) {
return (m & FULFILLING) != 0;
}
/**
* 当前节点是否已取消
* @return 结果值
*/
boolean isCancelled() {
return match == this;
}
}
/**
* 通过队列结构作为媒介来传递节点信息,其结构的特性是先进先出,意思是先入队列的节点会先匹配
* 该片段代码比TransferStack类更加复杂难懂羞涩,感觉是不是没写好
*/
static final class TransferQueue<E> extends Transferer<E> {
// 队列的头部节点
transient volatile QNode head;
// 队列的尾部节点
transient volatile QNode tail;
// 若清除节点是尾部节点,则该值将会是清除节点的上一个节点,目的是为了能够在下次清除它
transient volatile QNode cleanMe;
// 节点信息
static final class QNode {
// 下一个节点
volatile QNode next;
// 当前节点的值
volatile Object item;
// 控制park/unpark
volatile Thread waiter;
// put/take模式,生产者/消费者
final boolean isData;
/**
* 初始化
* @param item 节点的值
* @param isData 模式
*/
QNode(Object item, boolean isData) {
this.item = item;
this.isData = isData;
}
/**
* 更新当前节点的下一个节点为指定节点
* @param cmp 旧下一个节点
* @param val 新下一个节点
* @return 结果值
*/
boolean casNext(QNode cmp, QNode val) {
return next == cmp && UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
/**
* 更新当前节点的值为指定值
* @param cmp 旧值
* @param val 新值
* @return 结果值
*/
boolean casItem(Object cmp, Object val) {
return item == cmp && UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
}
/**
* 取消当前节点,使其值指向自身
* @param cmp 指定值
*/
void tryCancel(Object cmp) {
UNSAFE.compareAndSwapObject(this, itemOffset, cmp, this);
}
/**
* 当前节点是否已取消
* @return 结果值
*/
boolean isCancelled() {
return item == this;
}
/**
* 判断当前节点是否已经出队列
* @return 结果值
*/
boolean isOffList() {
return next == this;
}
}
/**
* 初始化
*/
TransferQueue() {
QNode h = new QNode(null, false);
head = h;
tail = h;
}
/**
* 生产者添加节点,消费者获取生产者生产的产品,所以put/take都会调用此方法
* 若是匹配成功,当前节点是生产者则最终返回自身的数据,当前节点是消费者则最终返回生产者的数据(产品)
* 若是当前节点被取消了,则返回null
* @param e 当前节点的值
* @param timed 是否指定了阻塞时间
* @param nanos 阻塞时间
* @return 结果值,参考注释
*/
E transfer(E e, boolean timed, long nanos) {
QNode s = null;
boolean isData = (e != null); // put -> isData = true(生产者) take -> isData = false (消费者))
for (;;) {
QNode t = tail;
QNode h = head;
if (t == null || h == null)
continue;
/**
* 1. 队列未初始化,也就是说此时没有生产者也没有消费者,因为put/take都有可能调用此方法,只是模式不同
* 2. 已存在节点,这个时又添加了新的节点,那么就要已存在节点与新节点是否能够匹配,即能够组成一对,有生产者也要有消费者,只有这样子的情况下才有可能匹配成功
* 如果两者的模式是一样的,那就压根不用判断了,直接入队列
*/
if (h == t || t.isData == isData) { // 进入到这里说明队列中还未能匹配成一对
QNode tn = t.next;
if (t != tail)
continue;
if (tn != null) { // 出现这种情况是因为其他线程先是对tail的下一个节点进行了赋值,还未马上修改tail的指向,所以当前线程就顺带修改了
advanceTail(t, tn); // 更新tail为其下一个节点
continue;
}
if (timed && nanos <= 0) // 进入到这里说明队列中还未能匹配上,在不等待的前提下最终只能是返回null
return null;
if (s == null)
s = new QNode(e, isData);
if (!t.casNext(null, s)) // 关联关系t.next = s
continue;
advanceTail(t, s); // 更新tail为其下一个节点
Object x = awaitFulfill(s, e, timed, nanos);
if (x == s) { // awaitFullfill只可能返回已匹配的节点或自身节点,若是自身节点说明被取消了
clean(t, s);
return null;
}
if (!s.isOffList()) { // 走到这里说明匹配成功,需要将该节点出队列,判断是否已经出队列
advanceHead(t, s); // 更新head为其下一个节点
if (x != null)
s.item = s;
s.waiter = null;
}
return (x != null) ? (E)x : e; // 若当前节点是消费者自然就能获取生产者的数据,若是生产者自然就只能返回自身的数据了
} else { // 走到这里说明已经可以匹配了
QNode m = h.next;
if (t != tail || m == null || h != head)
continue;
Object x = m.item;
/**
* 能走到这里说明m要么是生产者,当前节点是消费者,m要么是消费者,当前节点是生产者,可以组成一对了
* isData == ( x != null) 的出现是因为上一个线程正好修改了x的值
* 假设m属于生产者,当前节点是消费者,其m.item != null, e == null,同时有多个消费者线程进入到这里,上一个线程将m.item = null,所以下一个线程获取到的x = null
* 导致 x != null的结果是false,而isData是false,所以当isData == ( x != null)成立时说明m已经被匹配过了
* x == m 说明m被取消了
* m.casItem设置m.item为e,这里的e,如果是生产者则是数据,消费者则是null,所以m如果是生产者,则item变为null,消费者则变为生产者的数据
*/
if (isData == (x != null) || x == m || !m.casItem(x, e)) {
advanceHead(h, m);
continue;
}
// 与m匹配成功,将m出队,并唤醒等待在m上的线程m.waiter
advanceHead(h, m);
LockSupport.unpark(m.waiter);
return (x != null) ? (E)x : e; // 若当前节点是消费者自然就能获取生产者的数据,若是生产者自然就只能返回自身的数据了
}
}
}
/**
* 通过自旋 + 阻塞的方式等待当前节点被匹配
* @param s 当前节点
* @param timed 是否指定了阻塞时间
* @param nanos 阻塞时间
* @return 已取消指向自身,匹配成功指向其他节点
*/
Object awaitFulfill(QNode s, E e, boolean timed, long nanos) {
final long deadline = timed ? System.nanoTime() + nanos : 0L;
Thread w = Thread.currentThread();
/**
* 可以知道队列的头部节点是其虚拟出来的或已取消的或已匹配的,总之就是无法使用的,只有其head.next才是实际上添加或待匹配的节点
* 所以如果当前节点不是head.next节点的话则不自旋,因为还不是它的轮次,毕竟队列的特性是先进先出,即使自旋了也是白白浪费CPU资源
*/
int spins = ((head.next == s) ? (timed ? maxTimedSpins : maxUntimedSpins) : 0);
for (;;) {
if (w.isInterrupted())
s.tryCancel(e); // 线程被中断取消当前节点,实际上就是让s.item = this 指向自身
Object x = s.item;
if (x != e) // 当前节点被匹配/取消,若节点被匹配了,那么x指向其他节点,若节点被取消了,那么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);
}
}
/**
* 1. 从头部节点开始清除已取消的节点直到遇到未取消的节点
* 2. 若指定节点s是非尾部节点则直接清除
* 3. 若指定节点s是尾部节点则通过cleanMe属性来延迟清除
* @param pred 清除节点的上一个节点
* @param s 清除节点
*/
void clean(QNode pred, QNode s) {
s.waiter = null;
while (pred.next == s) { // pred.next != s 说明其指向发生了变化,导致其指向发生变化的原因是因为调用了advanceHead方法,就是因为s被取消后引发调用advanceHead方法让其上一个节点指向自身,相当于s其实已经从队列中移除了
QNode h = head;
QNode hn = h.next;
if (hn != null && hn.isCancelled()) { // 从队列头部开始遍历,遇到被取消的节点则将其出队
advanceHead(h, hn);
continue;
}
QNode t = tail;
if (t == h) // 说明队列为空
return;
QNode tn = t.next;
if (t != tail) // 要求多线程下tail的一致性
continue;
if (tn != null) {
advanceTail(t, tn); // 更新tail为其下一个节点
continue;
}
if (s != t) { // 如果被取消的节点不是尾部节点的话就直接移除
QNode sn = s.next;
if (sn == s || pred.casNext(s, sn)) // sn == s说明由于其子节点被取消了,s节点已经在其他线程下被移除了
return;
}
/**
* 若当前清除的节点是尾部节点的话,那么它并不是马上对其移除,而是将当前节点的上一个节点保存下来,等到下次出现清除节点时在去清除
* 这里说下我对为什么不能马上清除尾部节点的理解:
* 如果马上清除尾部节点了,那么tail的指向是不是要指向其上一个节点tail = tail.pred,可惜QNode中并未有这个属性
* 当然了,这是可以加上的,假设 head = A,head.next = B,tail = B,多线程情况下当B节点被清除时,那么应该这样子 head = B,tail = A,结果是不是错误的?
* 所以是为了避免多线程情况下head/tail指向发生问题
*/
QNode dp = cleanMe;
if (dp != null) { //
QNode d = dp.next;
QNode dn;
/**
* 在cleanMe有值的情况下再次调用clean方法说明之前要清除的尾部节点现在已经不在是尾部节点了,也就是可以清理之前要清除的节点了
* d == null不知道是如何导致的?
* d == dp 说明之前要清除的节点已经被移除了,所以此时只要将cleanMe置null,那么此时的清除节点还未处理呢,是的,所以下次while循环时会将当前清除节点的上一个节点设置到cleanMe上
* !d.isCancelled 说明之前要清除的节点又被反悔了??? 不知道什么情况下出现...
* d != t && (dn = d.next) != null && dn != d保证之前要清除的节点不是尾部节点,最后通过dp.casNext将其清除
*/
if (d == null || d == dp || !d.isCancelled() || (d != t && (dn = d.next) != null && dn != d && dp.casNext(d, dn)))
casCleanMe(dp, null);
if (dp == pred)
return; // s is already saved node
} else if (casCleanMe(null, pred)) // 一开始cleanMe = null,此时将当前要需要的上一个节点赋值给cleanMe
return; // Postpone cleaning s
}
}
/**
* 更新尾部节点为指定节点
* @param t 尾部节点
* @param nt 指定节点
*/
void advanceTail(QNode t, QNode nt) {
if (tail == t)
UNSAFE.compareAndSwapObject(this, tailOffset, t, nt);
}
/**
* 更新头部节点为指定节点
* @param h 旧头部节点
* @param nh 新头部节点
*/
void advanceHead(QNode h, QNode nh) {
if (h == head && UNSAFE.compareAndSwapObject(this, headOffset, h, nh))
h.next = h; // forget old next
}
/**
* 更新cleanMe为指定节点
* @param cmp 旧cleanMe
* @param val 新cleanMe
* @return 结果值
*/
boolean casCleanMe(QNode cmp, QNode val) {
return cleanMe == cmp && UNSAFE.compareAndSwapObject(this, cleanMeOffset, cmp, val);
}
}
/**
* 插入节点
* @param e 节点
*/
public void put(E e) throws InterruptedException {
if (e == null)
throw new NullPointerException();
if (transferer.transfer(e, false, 0) == null) { // 只有在当前线程被中断的情况下会返回null
Thread.interrupted();
throw new InterruptedException();
}
}
/**
* 插入节点
* @param e 节点
* @param timeout 指定时间
* @param unit 时间单位
* @return 结果值
*/
public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException {
if (e == null)
throw new NullPointerException();
if (transferer.transfer(e, true, unit.toNanos(timeout)) != null)
return true;
if (!Thread.interrupted()) // 判断是因为超时被取消还是当前线程被中断了
return false;
throw new InterruptedException();
}
/**
* 插入节点
* @param e 节点
* @return 结果值
*/
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
return transferer.transfer(e, true, 0) != null;
}
/**
* 获取元素
* @return 结果值
*/
public E take() throws InterruptedException {
E e = transferer.transfer(null, false, 0);
if (e != null)
return e;
Thread.interrupted();
throw new InterruptedException();
}
/**
* 获取元素
* @return 结果值
*/
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
E e = transferer.transfer(null, true, unit.toNanos(timeout));
if (e != null || !Thread.interrupted())
return e;
throw new InterruptedException();
}
/**
* 获取元素
* @return 结果值
*/
public E poll() {
return transferer.transfer(null, true, 0);
}
// 因为其内部没有容量,所以并不支持contains/remove/
总结
SynchronousQueue是典型的生产者/消费者模式,只有在生产者与消费者交易成功后才算是完成一单交易,其中有两种模式:
-
非公平策略使用
栈作为数据结构,其特性是先进后出,后进的生产者与消费者将会优先交易。 -
公平策略使用
队列作为数据结构,其特性是先进先出,先进的生产者与消费者将会优先交易。
SynchronousQueue的应用场景应该是一个线程负责生产数据,其他线程负责接收数据,两者的速率最好均衡,否则很容易出现大量阻塞的情况。
浙公网安备 33010602011771号