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();
/**
* 生产方法
*/
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);
Thread.sleep(3000);
executor.execute(consumerThread);
executor.shutdown();
}
}
控制台打印信息:
pool-1-thread-2消费了1617525421976
pool-1-thread-1生产了1617525421976
3 构造函数
SynchronousQueue默认采用非公平模式
public SynchronousQueue() {
//默认是非公平模式
this(false);
}
public SynchronousQueue(boolean fair) {
//true:公平模式,使用FIFO队列进行存储数据
//false:f非公平模式,使用LIFO栈进行存储数据
transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
4 存储数据节点SNode
static final class SNode {
//指向下一个节点
volatile SNode next; // next node in stack
//存储当前节点的匹配节点
//match为当前节点本身,说明当前节点被取消了
volatile SNode match; // the node matched to this
//存储当前线程,用于阻塞/被唤醒
volatile Thread waiter; // to control park/unpark
//数据项,若item存储的数据为节点本身,说明该节点被取消了
Object item; // data; or null for REQUESTs
//节点的模式:
//1)mode = 0, 属于REQUEST模式,即取出模式
//2)mode = 1,属于DATA模式 ,插入模式
//3)mode = 2,属于FULFILLING模式,说明此节点找到与之匹配的节点。若当前节点是取出,则找到添加节点,反之亦然。
int mode;
//有参构造函数
SNode(Object item) {
this.item = item;
}
//通过CAS更新下一个节点
boolean casNext(SNode cmp, SNode val) {
return cmp == next &&
UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
//当前节点尝试与节点s进行匹配
boolean tryMatch(SNode s) {
//match等于null,说明当前节点还没有被匹配到
if (match == null &&
//通过CAS操作将当前节点的match指向s
UNSAFE.compareAndSwapObject(this, matchOffset, null, s)) {
//匹配成功
//w为当前节点存储的的阻塞线程
Thread w = waiter;
//w非空
if (w != null) { // waiters need at most one unpark
//将waiter上的引用清空
waiter = null;
//唤醒等待线程w
LockSupport.unpark(w);
}
//返回匹配成功
return true;
}
//匹配失败
//判断当前节点的match引用是否为s
return match == s;
}
//尝试取消当前节点
//取消方式是使当前节点保存
void tryCancel() {
UNSAFE.compareAndSwapObject(this, matchOffset, null, this);
}
//判断当前节点是否被取消
boolean isCancelled() {
return match == this;
}
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long matchOffset;
private static final long nextOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> k = SNode.class;
matchOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("match"));
nextOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("next"));
} catch (Exception e) {
throw new Error(e);
}
}
}
5 添加操作(put方法)
public void put(E e) throws InterruptedException {
//非空检查
if (e == null) throw new NullPointerException();
//e:待插入元素 false:表示没有设置超时时间,超时时间不起作用 0:超时时间
if (transferer.transfer(e, false, 0) == null) {
//中断标记
Thread.interrupted();
//抛出异常
throw new InterruptedException();
}
}
6 取出操作(take方法)
跟put方法类似,都是调用transfer方法,唯一不同的是入参的元素为null
public E take() throws InterruptedException {
E e = transferer.transfer(null, false, 0);
//返回的数据不为mull
if (e != null)
//返回获取的数据
return e;
//返回的数据mull
//标记中断
Thread.interrupted();
//抛出异常
throw new InterruptedException();
}
7 图解节点的入队和出队
SQ的代码相对LBQ和ABQ复杂多了,在深入源码之前,建议先看下添加和取出的过程图。
executor.execute(productThread);
Thread.sleep(3000);
executor.execute(productThread);
Thread.sleep(3000);
executor.execute(consumerThread);
Thread.sleep(3000);
executor.execute(consumerThread);
1)t1执行put操作,new一个SNode1节点,并且将SNode1入栈,head指向SNode1,t1线程阻塞
2)t2执行put操作,发现头节点不为空,且头节点的模式跟自己一样。t2 new一个SNode2节点,并且将SNode2入栈,head指向SNode2,t2线程阻塞
3) t3执行take操作,不直接将SNode2取出,而是new一个SNode3,并且将SNode3压入栈
SNode3去找匹配节点,发现SNode2是put操作,跟自己匹配,两个节点一起出队,唤醒t2
4)t4执行take操作,new一个SNode4,并且将SNode4压入栈
SNode4去找匹配节点,发现SNode1是put操作,跟自己匹配,两个节点一起出队,唤醒t1
8 取出和添加的公共操作
8.1 transfer方法
E transfer(E e, boolean timed, long nanos) {
//待插入节点
SNode s = null; // constructed/reused as needed
//通过put方法进来,e不为null,因此待插入节点模式为DATA
int mode = (e == null) ? REQUEST : DATA;
//无限制循环
for (;;) {
//将头节点赋予h
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;
//s为空则创建新的节点
//修改s节点的模式,并将s下一个节点的引用指向h
//通过CAS操作将头节点从h更新为s
} else if (casHead(h, s = snode(s, e, h, mode))) {
//等待直到有另外一个线程执行相反的操作,put对应take,反之亦然
SNode m = awaitFulfill(s, timed, nanos);
//m为s的match值,m等于s说明s被取消了
if (m == s) { // wait was cancelled
//将s从栈中移除
clean(s);
//返回空
return null;
}
//头节点不为空且头节点的下一个节点指向s
if ((h = head) != null && h.next == s)
//s节点匹配成功,通过CAS将s的下一个节点更新为头节点
casHead(h, s.next); // help s's fulfiller
//如果当前线程的调用的是取出方法进来,即REQUEST模式,返回m.item;否则返回s.item
return (E) ((mode == REQUEST) ? m.item : s.item);
}
//判断h是否为FULFILLING模式
} else if (!isFulfilling(h.mode)) { // try to fulfill
//h被取消
if (h.isCancelled()) // already cancelled
//将h弹出栈
casHead(h, h.next); // pop and retry
//s为空则创建新的节点
//修改s节点的模式为FULFILLING|mode,并将s下一个节点的引用指向h
//通过CAS操作将头节点从h更新为s
e
//无限循环
for (;;) { // loop until matched or waiters disappear
//s的下一个节点
SNode m = s.next; // m is s's match
//m为空
if (m == null) { // all waiters are gone
//将头节点从s更新为null
casHead(s, null); // pop fulfill node
//释放s节点
s = null; // use new node next time
//返回最外层的for循环
break; // restart main loop
}
//mn为s的下下个节点
SNode mn = m.next;
//m尝试与节点s进行匹配
if (m.tryMatch(s)) {
//匹配成功,将头节点从s更新为mn,让s和m出栈
casHead(s, mn); // pop both s and m
//如果当前线程的调用的是取出方法进来,即REQUEST模式,返回m.item;否则返回s.item
return (E) ((mode == REQUEST) ? m.item : s.item);
} else // lost match
//m节点与其他节点已经匹配,将s的下一个节点指向mn
s.casNext(m, mn); // help unlink
}
}
//走到这一步,说明栈顶的模式为FULFILLING,而且没有出队, 此时需要帮助栈顶找到匹配节点
} else { // help a fulfiller
SNode m = h.next; // m is h's match
//h的下一个节点为空
if (m == null) // waiter is gone
//将头节点更新为空
casHead(h, null); // pop fulfilling node
else {
SNode mn = m.next;
//m与h进行匹配
if (m.tryMatch(h)) // help match
//匹配成功,将h和m出栈
casHead(h, mn); // pop both h and m
else // lost match
//匹配不成功,将h的下一个节点指向mn
h.casNext(m, mn); // help unlink
}
}
}
}
8.2 snode方法
static SNode snode(SNode s, Object e, SNode next, int mode) {
//s为null则创建节点
if (s == null) s = new SNode(e);
//将s节点的模式修改为mode
s.mode = mode;
//将s节点下一个节点的引用指向next节点
s.next = next;
return s;
}
8.3 casHead
更新栈的头节点
boolean casHead(SNode h, SNode nh) {
//判断h是否等于头节点,若不是说明被其他线程修改过
return h == head &&
//使用CAS将nh替换h
UNSAFE.compareAndSwapObject(this, headOffset, h, nh);
}
8.4 awaitFulfill方法
//获取服务器的核心数
static final int NCPUS = Runtime.getRuntime().availableProcessors();
//当前线程阻塞前的自旋次数,现在的服务器基本属于多核服务器,因此maxTimedSpins一般为32
static final int maxTimedSpins = (NCPUS < 2) ? 0 : 32;
//当前线程阻塞前的自旋次数,多核服务器下,maxUntimedSpins一般为32 * 16 = 512
static final int maxUntimedSpins = maxTimedSpins * 16;
//自旋超时阀值,超过这个数值的才会发生阻塞
static final long spinForTimeoutThreshold = 1000L;
SNode awaitFulfill(SNode s, boolean timed, long nanos) {
//超时时间,time为false时,deadline为0
final long deadline = timed ? System.nanoTime() + nanos : 0L;
//获取当前线程
Thread w = Thread.currentThread();
//通过shouldSpin判断是否需要自旋,ture为需要
//spins表示自旋次数
int spins = (shouldSpin(s) ?
(timed ? maxTimedSpins : maxUntimedSpins) : 0);
//无限循环
//自旋
for (;;) {
//判断当前线程是否被中断
if (w.isInterrupted())
//将当前节点的match指向当前节点
s.tryCancel();
//将s的匹配节点赋予m
SNode m = s.match;
//匹配节点不为空
if (m != null)
//直接返回匹配的节点
return m;
//超时处理
if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
s.tryCancel();
continue;
}
}
//判断自旋次数是否大于0
if (spins > 0)
//shouldSpin为false,表示其他线程更改过头节点
//shouldSpin为true,自旋次数减去1
spins = shouldSpin(s) ? (spins-1) : 0;
//自旋次数为0且当前节点的waiter为空
else if (s.waiter == null)
//将当前线程添加到s的waiter中
s.waiter = w;
//自旋次数为0且没有设置超时时间
else if (!timed)
//阻塞当前线程
//通过s节点的waiter可以找到阻塞的线程并将其唤醒
LockSupport.park(this);
//如果nanos小于spinForTimeoutThreshold,因为1000纳秒的超时等待无法做到十分的精确,而且非常断,干脆不发生阻塞
else if (nanos > spinForTimeoutThreshold)
//阻塞指定超时时间
LockSupport.parkNanos(this, nanos);
}
}
8.5 shouldSpin方法
判断是否自旋
boolean shouldSpin(SNode s) {
SNode h = head;
//满足三者之一:1)s节点等于头节点 2)头节点为空 3)头节点的模式为FULFILLING
//这样设计有个好处,因为非公平模式采用栈的结构,当节点s已经非头节点,说明它不是马上出队的节点,因此不需要再浪费CPU了
return (h == s || h == null || isFulfilling(h.mode));
}
8.6 isFulfilling方法
判断节点模式是否为FULFILLING模式
static boolean isFulfilling(int m) { return (m & FULFILLING) != 0; }
8.7 clean方法
删除节点s,最差条件下,可能需要遍历整个栈才能删除s
//将s从栈中移除
void clean(SNode s) {
//清空i数据项
s.item = null; // forget item
//清空waiter中存储的线程
s.waiter = null; // forget thread
//s的下一个节点
SNode past = s.next;
//past不为空且past被取消
if (past != null && past.isCancelled())
//past为s的下下个节点
past = past.next;
// Absorb cancelled nodes at head
SNode p;
//从栈顶开始移除被取消的节点
while ((p = head) != null && p != past && p.isCancelled())
//(p为被取消节点,将p出队
casHead(p, p.next);
//将p节点之后,past节点之前的被取消节点移出栈
while (p != null && p != past) { // Unsplice embedded nodes
SNode n = p.next;
//节点n不为空且n被取消
if (n != null && n.isCancelled())
//将p的下一个节点指向n的下一个节点
p.casNext(n, n.next);
else
//若n为空,终止while循环
p = n;
}
}
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年,机械工业出版社