BlockingQueue 实现之 SynchronousQueue

SynchronousQueue是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除操作take,反过来也一样。

不像ArrayBlockingQueue或LinkedListBlockingQueue,SynchronousQueue内部并没有数据缓存空间,你不能调用peek()方法来看队列中是否有数据元素,因为数据元素只有当你试着取走的时候才可能存在,不取走而只想偷窥一下是不行的,当然遍历这个队列的操作也是不允许的。队列头元素是第一个排队要插入数据的线程,而不是要交换的数据。数据是在配对的生产者和消费者线程之间直接传递的,并不会将数据缓冲数据到队列中。可以这样来理解:生产者和消费者互相等待对方,握手,然后一起离开。

SynchronousQueue的一个使用场景是在线程池里。Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。

接下来,我们来看看具体的源码实现吧,它的源码不是很简单的那种,我们需要先搞清楚它的设计思想。

我们先看大框架:

 // 构造时,我们可以指定公平模式还是非公平模式,本文主要讲解公平模式
public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue() : new TransferStack();
}
abstract static class Transferer {
// 从方法名上大概就知道,这个方法用于转移元素,从生产者手上转到消费者手上
// 也可以被动地,消费者调用这个方法来从生产者手上取元素
// 第一个参数 e 如果不是 null,代表场景为:将元素从生产者转移给消费者
// 如果是 null,代表消费者等待生产者提供元素,然后返回值就是相应的生产者提供的元素
// 第二个参数代表是否设置超时,如果设置超时,超时时间是第三个参数的值
// 返回值如果是 null,代表超时,或者中断。具体是哪个,可以通过检测中断状态得到。
abstract Object transfer(Object e, boolean timed, long nanos);
} TransferQueue() {
//初始化时,head和tail都是空节点
QNode h = new QNode(null, false); // initialize to dummy node.
head = h;
tail = h;
}

Transferer 有两个内部实现类,是因为构造 SynchronousQueue 的时候,我们可以指定公平策略。公平模式意味着,所有的读写线程都遵守先来后到,FIFO 嘛,对应 TransferQueue。而非公平模式则对应 TransferStack。

本文我们主要看公平模式源码,接下来,我们看看 put 方法和 take 方法:

 // 写入值
public void put(E o) throws InterruptedException {
if (o == null) throw new NullPointerException();
if (transferer.transfer(o, false, 0) == null) { //
Thread.interrupted();
throw new InterruptedException();
}
}
// 读取值并移除
public E take() throws InterruptedException {
Object e = transferer.transfer(null, false, 0); //
if (e != null)
return (E)e;
Thread.interrupted();
throw new InterruptedException();
}

我们看到,写操作 put(E o) 和读操作 take() 都是调用 Transferer.transfer(…) 方法,区别在于第一个参数是否为 null 值。

我们来看看 transfer 的设计思路,其基本算法如下:

  1. 当调用这个方法时,如果队列是空的,或者队列中的节点和当前的线程操作类型一致(如当前操作是 put 操作,而队列中的元素也都是写线程)。这种情况下,将当前线程加入到等待队列并阻塞线程
  2. 如果队列中有等待节点,而且与当前操作可以匹配(1、如队列中都是读操作线程,当前线程是写操作线程;2、如队列中都是写操作线程,当前线程是读操作;)。这种情况下,匹配等待队列的队头,出队,返回相应数据

其实这里有个隐含的条件被满足了,队列如果不为空,肯定都是同种类型的节点,要么都是读操作,要么都是写操作。这个就要看到底是读线程积压了,还是写线程积压了。

我们可以假设出一个男女配对的场景:

  1、一个男的过来,如果一个人都没有,那么他需要等待;如果发现有一堆男的在等待,那么他需要排到队列后面;如果发现是一堆女的在排队,那么他直接牵走队头的那个女的;

  2、相反一个女的过来,如果一个人都没有,那么她需要等待;如果发现有一堆女的在等待,那么她需要排到队列后面;如果发现是一堆男的在排队,那么队头的那个男的直接出队牵走这个女的;

既然这里说到了等待队列,我们先看看其实现,也就是 QNode:

 static final class QNode {
volatile QNode next; // 可以看出来,等待队列是单向链表
volatile Object item; // CAS'ed to or from null
volatile Thread waiter; // 将线程对象保存在这里,用于挂起和唤醒
final boolean isData; // 用于判断是写线程节点(isData == true),还是读线程节点 QNode(Object item, boolean isData) {
this.item = item;
this.isData = isData;
}
......

我们再来看 transfer 方法的代码:

 /**
* Puts or takes an item.
*/
Object transfer(Object 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 // 走到这里,说明已经初始化完成,但是初始化时head = h;tail = h;head和tail都是相同的空节点
// 如果h == t为false,则判断t.isData == isData,判断队尾节点和当前节点类型是否一致
// 队列空,或队列中节点类型和当前节点一致,
// 即我们说的第一种情况,将节点入队即可。读者要想着这块 if 里面方法其实就是入队
if (h == t || t.isData == isData) { // empty or same-mode
QNode tn = t.next;
// t != tail 说明刚刚有节点入队,continue 即可
if (t != tail) // inconsistent read
continue;
// 有其他节点入队,但是 tail 还是指向原来的,此时设置 tail 即可
if (tn != null) { // lagging tail
// 这个方法就是:如果 tail 此时为 t 的话,设置为 tn
advanceTail(t, tn);
continue;
}
//
if (timed && nanos <= 0) // can't wait
return null;
// s == null,则创建一个新节点
if (s == null)
s = new QNode(e, isData);
// 将当前节点,插入到 tail 的后面
if (!t.casNext(null, s)) // failed to link in
continue; // 将当前节点设置为新的 tail
advanceTail(t, s); // swing tail and wait
// 看到这里,请读者先往下滑到这个方法,看完了以后再回来这里,思路也就不会断了
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
advanceHead(t, s); // unlink if head
if (x != null) // and forget fields
s.item = s;
s.waiter = null;
}
return (x != null) ? x : e; // 这里的 else 分支就是上面说的第二种情况,有相应的读或写相匹配的情况
} else { // complementary-mode
QNode m = h.next; // node to fulfill
// 不一致读,表明有其他线程修改了队列
if (t != tail || m == null || h != head)
continue; // inconsistent read Object x = m.item;
if (isData == (x != null) || // m already fulfilled
x == m || // m cancelled
!m.casItem(x, e)) { // lost CAS
advanceHead(h, m); // dequeue and retry
continue;
} advanceHead(h, m); // successfully fulfilled
LockSupport.unpark(m.waiter);
return (x != null) ? x : e;
}
}
} void advanceTail(QNode t, QNode nt) {
if (tail == t)
UNSAFE.compareAndSwapObject(this, tailOffset, t, nt);
}

注意44行e为即将要挂起线程的node的值,如果是put,则为其传的值,如果是get,则是null。

 // 自旋或阻塞,直到满足条件,这个方法返回
Object awaitFulfill(QNode s, Object e, boolean timed, long nanos) { long lastTime = timed ? System.nanoTime() : 0;
Thread w = Thread.currentThread();
// 判断需要自旋的次数,
int spins = ((head.next == s) ?
(timed ? maxTimedSpins : maxUntimedSpins) : 0);
for (;;) {
// 如果被中断了,那么取消这个节点
if (w.isInterrupted())
// 就是将当前节点 s 中的 item 属性设置为 this
s.tryCancel(e);
Object x = s.item;
// 这里是这个方法的唯一的出口
if (x != e)
return x;
// 如果需要,检测是否超时
if (timed) {
long now = System.nanoTime();
nanos -= now - lastTime;
lastTime = now;
if (nanos <= 0) {
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);
}
} void tryCancel(Object cmp) {
//将节点item设置为自己,代表此节点取消排队
UNSAFE.compareAndSwapObject(this, itemOffset, cmp, this);
}

我们看第14行,上面我们已经说过第44行e为即将要挂起线程的node的值,如果是put,则为其传的值,如果是get,则是null。则如果队列里都是相同的操作类型,会直接挂起,再看上上个方法的第二个else分支,也就是不同的操作类型的时候,会直接获取队列的第一个等待节点,并且第70行,通过cas设置其节点的item为本次操作的值,也就是如果本次操作为put,则队列的节点为take类型,也就是队列节点的item值为null,现在通过cas设置其值,然后再将其唤醒,则awaitFulfill唤醒后,第17行处节点的值已经被cas更改了,自然不能和原始的e相等,就会直接返回给take。

现在我们来按照实际情况来走一遍流程:

  1、线程1初始化 new SynchronousQueue(true) ,调用 put(E o)写入值,我们看 transfer(Object e, boolean timed, long nanos) 方法第7行,isData 为true,接在到第20行,因为是刚刚初始化,tail和head都为空节点,36行新建一个节点,38行将当前节点,插入到 tail 的后面,42行将当前节点设置为新的 tail,所以队列中有三个节点,两个是空节点,一个是当前节点,我们再看到 awaitFulfill(QNode s, Object e, boolean timed, long nanos) 方法第16行,此时x = e,不返回,36行处挂起当前线程。

       此时线程2调用 take() 取值,我们看 transfer(Object e, boolean timed, long nanos) 方法第7行,isData 为false,接在到第20行,tail和head不相同,之前线程1写入时,QNode 的isData 为true,所以 if (h == t || t.isData == isData) 不满足,进入到transfer的62行,取到头节点的后面一个节点,很明显,这个节点的item是null,68行处x != null为false,  isData == (x != null) 为true,则执行71行将头节点后移一位,相当于去除了头结点,跳出循环继续;这一次循环第62行取到的是线程1中添加的节点,x != null为true,isData == (x != null)为false,x == m 为false,执行 m.casItem(x, e) ,此时e为null,将线程1中添加的节点的item的值设置为null,此时成功,不执行72行处,我们可以看到75行处,将头结点后移一位,相当于线程一put进去的值被移除了,76行处唤醒线程1,刚才说过这次循环62行取到的是线程1中添加的节点,68行处x为线程一中添加的元素,77行 return (x != null) ? x : e;  x != null 为true,则retrue x,此时take()拿到了线程1中添加的元素,并唤醒线程1,将线程一添加的节点去除,take()方法结束。我们再来看看线程1被唤醒后,看 awaitFulfill 方法中第36行,被唤醒后接着for循环,第14行获取被挂起之前添加的节点中的item,可是上面讲的线程2中m.casItem(x, e) 已经将此节点的e设置为null ,则17行处retrue null,再到transfer 方法44行处,X为null,58行处返回e,put方法结束。

  2、线程1初始化 new SynchronousQueue(true) ,调用 take() 取值,我们看 transfer(Object e, boolean timed, long nanos) 方法第7行,isData 为false,,因为是刚刚初始化,tail和head都为空节点,36行新建一个节点,38行将当前节点,插入到 tail 的后面,42行将当前节点设置为新的 tail,所以队列中有三个节点,两个是空节点,一个是当前节点,我们再看到 awaitFulfill(QNode s, Object e, boolean timed, long nanos) 方法第16行,此时x = e =null,不返回,36行处挂起当前线程。

       此时线程2调用 put(E o)写入值,我们看 transfer(Object e, boolean timed, long nanos) 方法第7行,isData 为true,接在到第20行,tail和head不相同,之前线程1取值时,QNode 的isData 为false,所以 if (h == t || t.isData == isData) 不满足,进入到transfer的62行,取到头节点的后面一个节点,很明显,这个节点的item是null,68行处x != null为false,  isData == (x != null) 为true,则执行71行将头节点后移一位,相当于去除了头结点,跳出循环继续;这一次循环第62行取到的是线程1中等待取值的节点,x != null为false,isData == (x != null)为false,x == m 为false,执行 m.casItem(x, e) ,将线程1中take()的节点的item的值设置为e,此时成功,不执行72行处,我们可以看到75行处,将头结点后移一位,相当于线程1取到值后就将线程从等待队列中移除了,76行处唤醒线程1,刚才说过这次循环62行取到的是线程1中等待取值的节点,68行处x为null,77行 return (x != null) ? x : e;  x != null 为false,则 retrue e,此时put()方法给线程1的QNode设置了item为e,并唤醒线程1,将线程一添加的节点去除,take()方法结束。我们再来看看线程1被唤醒后,看 awaitFulfill 方法中第36行,被唤醒后接着for循环,第14行获取被挂起之前添加的节点中的item,可是上面讲的线程2 put() 中m.casItem(x, e) 已经将此节点的e设置为 e ,则17行处retrue 的值为put()的 e ,再到transfer 方法44行处,X为e,58行处返回X,也就是put()的e ,take方法结束。

我们再来看看offer()和poll()

 public boolean offer(E e) {
if (e == null) throw new NullPointerException();
return transferer.transfer(e, true, 0) != null;
}

我们可以看出,也是调用了 transfer 方法,如果队列为空了,第一次offer添加元素的话,transfer第32行,timed =true,nanos =0,则此时reture null;

 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();
}

带超时的poll,如果队列为空,则会添加到等待队列,并且阻塞,经过 timeout 还没有拿到元素,则超时返回false,拿到了元素则返回元素。

由此可见,只有线程在等待着取元素, offer(E e) 才有可能成功,如果没有线程等待着取,则一定会返回失败。

总结

1、线程做相同类型的操作:

    多个线程 take() ,则将线程包装成QNode节点,item为null,将节点添加到队列,将线程挂起;

    多个线程 put() ,将线程包装成QNode节点,item为 e,将节点添加到队列,将线程挂起。

2、线程做不同类型的操作:

    有线程先做了put() ,其他线程做take() 操作时,take取到队列中的第一个等待节点中的item,take返回item,并将第一个等待节点唤醒,put返回e;

    有线程先做了take() ,其他线程put() 操作时,put将元素e赋值给队列中第一个等待节点的item,put返回e,并将第一个等待节点唤醒,take返回e。

 

并发编程(十)—— Java 并发队列 BlockingQueue 实现之 SynchronousQueue源码分析的更多相关文章

  1. 并发编程(九)—— Java 并发队列 BlockingQueue 实现之 LinkedBlockingQueue 源码分析

    LinkedBlockingQueue 在看源码之前,通过查询API发现对LinkedBlockingQueue特点的简单介绍: 1.LinkedBlockingQueue是一个由链表实现的有界队列阻 ...

  2. 并发编程(八)—— Java 并发队列 BlockingQueue 实现之 ArrayBlockingQueue 源码分析

    开篇先介绍下 BlockingQueue 这个接口的规则,后面再看其实现. 阻塞队列概要 阻塞队列与我们平常接触的普通队列(LinkedList或ArrayList等)的最大不同点,在于阻塞队列的阻塞 ...

  3. 消息队列的一些场景及源码分析,RocketMQ使用相关问题及性能优化

    前文目录链接参考: 消息队列的一些场景及源码分析,RocketMQ使用相关问题及性能优化 https://www.cnblogs.com/yizhiamumu/p/16694126.html 消息队列 ...

  4. java中的==、equals()、hashCode()源码分析(转载)

    在java编程或者面试中经常会遇到 == .equals()的比较.自己看了看源码,结合实际的编程总结一下. 1. ==  java中的==是比较两个对象在JVM中的地址.比较好理解.看下面的代码: ...

  5. Java的三种代理模式&完整源码分析

    Java的三种代理模式&完整源码分析 参考资料: 博客园-Java的三种代理模式 简书-JDK动态代理-超详细源码分析 [博客园-WeakCache缓存的实现机制](https://www.c ...

  6. java 日志体系(四)log4j 源码分析

    java 日志体系(四)log4j 源码分析 logback.log4j2.jul 都是在 log4j 的基础上扩展的,其实现的逻辑都差不多,下面以 log4j 为例剖析一下日志框架的基本组件. 一. ...

  7. Java SPI机制实战详解及源码分析

    背景介绍 提起SPI机制,可能很多人不太熟悉,它是由JDK直接提供的,全称为:Service Provider Interface.而在平时的使用过程中也很少遇到,但如果你阅读一些框架的源码时,会发现 ...

  8. 【并发编程】Java并发编程传送门

    本博客系列是学习并发编程过程中的记录总结.由于文章比较多,写的时间也比较散,所以我整理了个目录贴(传送门),方便查阅. [并发编程系列博客传送门](https://www.cnblogs.com/54 ...

  9. 【并发编程】Java并发编程-看懂AQS的前世今生

    在我们可以深入学习AbstractQueuedSynchronizer(AQS)之前,必须具备了volatile.CAS和模板方法设计模式的知识,本文主要想从AQS的产生背景.设计和结构.源代码实现及 ...

随机推荐

  1. Invitation Cards spfa

    题意  n个点m条单向边   计算 从1到每一个点的权值(除了1) 和每个点回到1 的权值之和 正向跑一次 反向跑一次即可 #include<bits/stdc++.h> using na ...

  2. 027 storm面试小题

    1.大纲 Storm工作原理是什么? 流的模式是什么?默认是什么? 对于mapreduce如何理解? Storm的特点和特性是什么? Storm组件有哪些? 2.Storm工作原理是什么? 相对于ha ...

  3. vue笔记-列表渲染

    用v-for把一个数组对应为一组元素 使用方法:v-for="(item,index) in items"//也可以使用of替代in { items:源数组 item:数组元素迭代 ...

  4. Sightseeing trip POJ - 1734 -Floyd 最小环

    POJ - 1734 思路 : Floyd 实质 dp ,优化掉了第三维. dp [ i ] [ j ] [ k ] 指的是前k个点优化后    i  ->  j   的最短路. 所以我们就可以 ...

  5. docker-compose模板文件参数说明

    working_dir:一般这个参数用在应用程序Services下,我们指定应用程序所在的目录为当前目录,类似linux中的cd working_dir.其余的参数,例如command等就是基于此参数 ...

  6. anaconda 命令集合

    0.安装 $ bash ~/Downloads/Anaconda3-5.3.1-MacOSX-x86_64.sh source ~/.bash_profile 1.查看 anaconda 的版本 co ...

  7. (一)stm32f103~~GPIO基本操作一(led灯)

    GPIO基本操作,如果更换IO口只需要更换端口设置即可 led.h文件 #ifndef __LED_H #ifndef __LED_H #define __LED_H #include "s ...

  8. VS 2013Ultimate 开发过程中遇到的问题——listbox的隐藏问题,combobox.textchanged的中文问题

    最近项目有需求,就是把一个combobox让它可以根据用户输入的汉字就行模糊查询出匹配的下拉框内容,很简单,是吧 但是!!!在开发中真是遇到了不少问题呢. 一.实现流程 1)我的实现思维是这样的,先把 ...

  9. vmware虚机 修改bios方法

    .vmx文件添加以下行,会在下次启动时自动出现bios,并会将"TRUE"改为“false” bios.forceSetupOnce = "TRUE"

  10. PRTG测试!

    http://www.paessler.com/prtg. 我的效果图: