1 概述
-
SynchronousQueue(简称SQ)是BlockingQueue接口的实现类,是一种阻塞、线程安全的、不存储任何元素的同步队列。SQ的底层实现分为两种,一种是非公平模式(默认),采用栈的数据结构,先入队的数据,后出队,即可LIFO;另外一种是公平模式,采用队列的数据结构,先入队的数据,先出队,即FIFO。
-
SQ跟LinkedBlockingQueue(LBQ)和ArrayBlockingQueue(ABQ)等阻塞队列,最大一个不同点是一个线程t1执行put操作后,必须有另外线程t2执行take操作,t1才能继续添加,否则t1会被阻塞。从这个角度去理解,才认为SQ本身不存储数据。在性能方面,由于SQ不存储数据,吞吐量高于LBQ和ABQ。
-
本文主要讲解SQ公平模式实现方法。
2 用例
使用SynchronousQueue队列可以快速构建一个生产者消费者的模型,因为生产者线程执行添加操作后会发生阻塞,除非有消费者线程取出才能会被唤醒。反之,如果消费者消费不到数据也会发生阻塞,直至有生产者执行添加操作。
public class SynchronousQueueApp {
//同步队列容器
private static SynchronousQueue queue = new SynchronousQueue(true);
/**
* 生产方法
*/
private static void product(){
try {
String content = System.currentTimeMillis() + "";
queue.put(content);
String tName = Thread.currentThread().getName();
System.out.println(tName + "生产了" + content);
}catch (InterruptedException e){
e.printStackTrace();
}
}
/**
* 消费方法
*/
private static void consumer(){
try {
Object content = queue.take();
String tName = Thread.currentThread().getName();
System.out.println(tName + "消费了" + content);
}catch (InterruptedException e){
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception{
ThreadPoolExecutor executor = new ThreadPoolExecutor(10,
10,
0,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>());
Thread productThread = new Thread(new Runnable() {
@Override
public void run() {
product();
}
});
Thread consumerThread = new Thread(new Runnable() {
@Override
public void run() {
consumer();
}
});
executor.execute(productThread);
executor.execute(consumerThread);
executor.shutdown();
}
}
控制台打印结果:
pool-1-thread-1生产了1617297399290
pool-1-thread-2消费了1617297399290
3 构造函数
无参构造函数默认使用非公平模式,即SynchronousQueue类的实现方法通过TransferStack;
公平模式需使用有参构造函数,且入参指定为true,SynchronousQueue类的实现方法为TransferQueue。
public SynchronousQueue() {
this(false);
}
public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
TransferQueue类是SynchronousQueue的内部类。在TransferQueue的构造函数中,会new一个新的QNode,并将head和tail引用指向同一个QNode。
TransferQueue() {
QNode h = new QNode(null, false); // initialize to dummy node.
head = h;
tail = h;
}
4 存储数据节点QNode
QNode是Transferer类的内部节点
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
//模式判断,true为添加操作,false为取出操作
final boolean isData;
//有参构造函数
QNode(Object item, boolean isData) {
this.item = item;
this.isData = isData;
}
//通过CAS操作更新下一个节点
boolean casNext(QNode cmp, QNode val) {
return next == cmp &&
UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
//通过CAS操作更新当前节点的数据项
boolean casItem(Object cmp, Object val) {
return item == cmp &&
UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
}
/**
* Tries to cancel by CAS'ing ref to this as item.
*/
//尝试取消当前节点
//实现原理是通过CAS操作将当前节点的数据项指向当前对象
void tryCancel(Object cmp) {
UNSAFE.compareAndSwapObject(this, itemOffset, cmp, this);
}
//判断当前节点是否被取消
boolean isCancelled() {
//判断节点的数据项是否指向当前对象,若是则被取消
return item == this;
}
/**
* Returns true if this node is known to be off the queue
* because its next pointer has been forgotten due to
* an advanceHead operation.
*/
//判断该节点是否出队
boolean isOffList() {
return next == this;
}
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long itemOffset;
private static final long nextOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> k = QNode.class;
itemOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("item"));
nextOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("next"));
} catch (Exception e) {
throw new Error(e);
}
}
}
5 图解节点的入队和出队
SQ的代码相对LBQ和ABQ复杂多了,在深入源码之前,建议先看下添加和取出的过程图。
第一步大家都一样,t1执行put操作,head和tail引用初始化,head和tail同时指向QNode1,此时item为空
5.1 put-take
executor.execute(productThread);
Thread.sleep(2000);
executor.execute(consumerThread);
1)t1将QNode1的next指向新增的QNode2,QNode2为队尾,tail引用指向QNode2。
此时因为没有其他线程执行take操作,t1就自旋一段时间后阻塞了
2)t2执行take操作,通过CAS将QNode2的item置为空,head和tail同时指向QNode,释放QNode1,唤醒t1,最后返回数据“data1”
5.2 take-put
executor.execute(consumerThread);
Thread.sleep(2000);
executor.execute(productThread);
1)t1将QNode1的next指向新增的QNode2,QNode2为队尾,tail引用指向QNode2。
此时因为没有其他线程执行put操作,t1就自旋一段时间后阻塞了
2)t2将QNode2的item更新为“data1”
3)t2让QNode1出队,h引用指向Onode2,唤醒t1
4)t1醒来后,将QNode2的item引用更新为QNode2本身,返回数据“data1”
5)看到这里你可能有疑问,那QNode2的item项目不为空了啊?没啥,如果后面又有新的线程成功执行完take操作,会让QNode2出队,通过GC将QNode2回收。
5.3 put-put-take-take
executor.execute(productThread);
Thread.sleep(2000);
executor.execute(productThread);
Thread.sleep(2000);
executor.execute(consumerThread);
Thread.sleep(2000);
executor.execute(consumerThread);
1)线程t2第二次put的时候,由于尾节点QNode2的模式和Qnode3的模式一样,都属于数据节点,因此QNode3会被添加到队列尾部
2)第一次执行take线程,会将QNode1出队,并将Qnode2得item置为空,唤醒线程t1
3)第二次执行take线程,会将QNode2出队,并将Qnode3得item置为空,唤醒线程t2
6 阻塞添加操作(put方法)
public void put(E e) throws InterruptedException {
//非空校验
if (e == null) throw new NullPointerException();
//和take方法类似,都是调用transferer.transfer方法,唯一不同的是第一个入参e非空
if (transferer.transfer(e, false, 0) == null) {
Thread.interrupted();
throw new InterruptedException();
}
}
7 阻塞式取出操作(take方法)
public E take() throws InterruptedException {
//和put方法类似,都是调用transferer.transfer方法,唯一不同的是第一个入参为null
E e = transferer.transfer(null, false, 0);
if (e != null)
return e;
Thread.interrupted();
throw new InterruptedException();
}
8 取出和添加的公共操作
8.1transfer方法
E transfer(E e, boolean timed, long nanos) {
//待插入队列节点
QNode s = null; // constructed/reused as needed
//put操作调用transfer,e不为null,isData为true
//take操作调用transfer,e为null,isData为false
boolean isData = (e != null);
//无限自旋
for (;;) {
//将尾节点赋予t
QNode t = tail;
//将头节点赋予t
QNode h = head;
//t或者h为空,重新自旋
if (t == null || h == null) // saw uninitialized value
continue; // spin
//1)当h==t,即队列为空 2)待插入节点模式与尾节点的模式相同
if (h == t || t.isData == isData) { // empty or same-mode
//获取t节点的下一个节点
QNode tn = t.next;
//t不等于tail,说明被其他线程改动过
if (t != tail) // inconsistent read
continue;
/ /tn不为空,说明被其他线程改动过
if (tn != null) { // lagging tail
//通过CAS,将tail节点的引用指向tn
advanceTail(t, tn);
//重新自旋
continue;
}
//超时处理
if (timed && nanos <= 0) // can't wait
return null;
//s为空,new一个节点并赋予s
if (s == null)
s = new QNode(e, isData);
//将s添加到尾巴节点的位置
if (!t.casNext(null, s)) // failed to link in
//重新自旋
continue;
//通过CAS,将tail节点的引用指向s
advanceTail(t, s); // swing tail and wait
//等待直到有另外一个线程执行相反的操作,put对应take,反之亦然
Object x = awaitFulfill(s, e, timed, nanos);
//x等于s,说明s节点被取消了
if (x == s) { // wait was cancelled
//从队头开始移除被取消的结点
clean(t, s);
return null;
}
//判断当前节点是否出被取消
if (!s.isOffList()) { // not already unlinked
//通过CAS将head点指向s
advanceHead(t, s); // unlink if head
//如果x不为空
if (x != null) // and forget fields
//is节点的itme指向自身
//说明item节点被取消了
s.item = s;
//将s节点中存储的等待线程清空
s.waiter = null;
}
//若x不为空,返回(E)x,否则返回e
return (x != null) ? (E)x : e;
//待插入节点模式与尾节点的模式不同相同
} else { // complementary-mode
//m为头节点的下一个节点
QNode m = h.next; // node to fulfill
//如果1)t不等于tail 2)m为空 3)h不等于头节点,说明已经有其他线程发生改变
if (t != tail || m == null || h != head)
//重新自旋
continue; // inconsistent read
//x为m中的数据项
Object x = m.item;
//1)x != null为true,说明x不等于空,m是数据节点。
如果isData等于true,说明待插入节点和头节点的类型一样,说明已经有其他线程发生改变
2)x == m,说明m节点被取消了
3)m.casItem(x, e)将m的item替换为e
if (isData == (x != null) || // m already fulfilled
x == m || // m cancelled
!m.casItem(x, e)) { // lost CAS
//h节点出队
advanceHead(h, m); // dequeue and retry
//重新自旋
continue;
}
//通过CAS,将head的引用指向m
advanceHead(h, m); // successfully fulfilled
//唤醒m中等阻塞的线程
LockSupport.unpark(m.waiter);
//x不为空,返回 (E)x,否则返回e
return (x != null) ? (E)x : e;
}
}
}
8.2 awaitFulfill方法
等待直到有另外一个线程成功执行匹配的操作,put对应take,反之亦然
Object awaitFulfill(QNode s, E e, boolean timed, long nanos) {
/* Same idea as TransferStack.awaitFulfill */
//超时时间,time为false时,deadline为0
final long deadline = timed ? System.nanoTime() + nanos : 0L;
//获取当前线程
Thread w = Thread.currentThread();
//spins为自旋次数
//判断s是否头节点,若是则需要自旋。这样设计有个好处,公平模式是先进先出,如果s前面还要其他节点,就没必要尝试自旋了,因为s不是第一个出队的元素
int spins = ((head.next == s) ?
(timed ? maxTimedSpins : maxUntimedSpins) : 0);
//无限循环
//自旋
for (;;) {
//判断当前线程是否被中断
if (w.isInterrupted())
//取消当前节点,即可将当前节点的item指向自己
s.tryCancel(e);
Object x = s.item;
//s节点中的item存储的元素不等于传入的e,,说明已经有另外一个线程成执行了匹配操作
if (x != e)
return x;
//超时处理
if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
s.tryCancel(e);
continue;
}
}
//自旋次数大于0
if (spins > 0)
// 执行自旋次数减1操作
--spins;
//s中的等待线程为空
else if (s.waiter == null)
//将waiter指向前线程
s.waiter = w;
//spins 为0,且没有设置超时时间
else if (!timed)
//阻塞当前线程
//如果有另外一个线程匹配成功,可以通过s节点的waiter可以找到阻塞的线程并将其唤醒
LockSupport.park(this);
//如果nanos小于spinForTimeoutThreshold,因为1000纳秒的超时等待无法做到十分的精确,而且非常断,干脆不发生阻塞
else if (nanos > spinForTimeoutThreshold)
//阻塞指定超时时间
LockSupport.parkNanos(this, nanos);
}
}
8.3 clean方法
从头开始移除被取消节点
//如果被取消的节点s是尾节点,clean是s的前驱节点,cleanMe的next引用指向s
transient volatile QNode cleanMe;
void clean(QNode pred, QNode s) {
//将s节点中包含的阻塞线程清空
s.waiter = null; // forget thread
/*无论什么时候,如果被取消的节点s是最后插入的节点,它是不能被删除。因此,我们使用cleanMe这个引用去保存s的前驱节点;
*后面清理时,如果发现s不是尾节点,就可以是用s的前驱节点的下一个节点指向s的下一个节点,从而释放s
*/
while (pred.next == s) { // Return early if already unlinked
//将头节点赋值给h
QNode h = head;
//头节点的下一个节点
QNode hn = h.next; // Absorb cancelled first node as head
//如果hn不为空且hn被取消
if (hn != null && hn.isCancelled()) {
//通过CAS将head指向hn
advanceHead(h, hn);
//重新自旋
continue;
}
//将尾节点赋值给t
QNode t = tail; // Ensure consistent read for tail
//h==t,说明队列为空
if (t == h)
//结束clean方法
return;
//t的下一个节点
QNode tn = t.next;
//t不等于tail,说明队列被他线程改变
if (t != tail)
//重新自旋
continue;
//tn 不为空,说明队列被他线程改变
if (tn != null) {
//通过CAS将tail指向tn
advanceTail(t, tn);
//重新自旋
continue;
}
//s不为尾节点,说明队列被他线程改变
if (s != t) { // If not tail, try to unsplice
//s的下一个节点
QNode sn = s.next;
//1)sn等于s 或 2)通过CAS释放sn成功
if (sn == s || pred.casNext(s, sn))
//结束clean方法
return;
}
QNode dp = cleanMe;
//移除被取消的节点
if (dp != null) { // Try unlinking previous cancelled node
//d是cleanMe的下一个节点
QNode d = dp.next;
//临时节点
QNode dn;
//d为空
if (d == null || // d is gone or
//d已经出队,dp的next指向自己
d == dp || // d is off list or
//d没有被取消
!d.isCancelled() || // d not cancelled or
//d!=t说明d不是尾节点
(d != t && // d not tail and
//dn是d的下一个节点且不为空
(dn = d.next) != null && // has successor
//d没有出队
dn != d && // that is on list
//t通过CAS将d的下一个节点指向d的下下个节点,说白了就说将d的下一个节点移除
dp.casNext(d, dn))) // d unspliced
//通过CAS将cleanMe置为空
casCleanMe(dp, null);
//dp == pred,说明s是最后一个插入的节点
if (dp == pred)
//结束clean方法
return; // s is already saved node
//cleanMe为空,说明s是最后一个插入的节点
} else if (casCleanMe(null, pred))
//结束clean方法
return; // Postpone cleaning s
}
}
9 参考文献
1)JDK7在线文档
https://tool.oschina.net/apidocs/apidoc?api=jdk_7u4
2) JDK8在线文档
https://docs.oracle.com/javase/8/docs/api/
3) Bruce Eckel. Java编程思想,第4版,2007,机械工业出版社
4)方腾飞,魏鹏,程晓明. Java并发编程的艺术,第1版,2015年,机械工业出版社