并发编程之 SynchronousQueue 核心源码分析
前言
SynchronousQueue
是一个普通用户不怎么常用的队列,通常在创建无界线程池(Executors.newCachedThreadPool()
)的时候使用,也就是那个非常危险的线程池 ^_^
。
它是一个非常特殊的阻塞队列,他的模式是:在 offer
的时候,如果没有另一个线程在 take 或者 poll
的话,就会失败,反之,如果在 take
或者 poll
的时候,没有线程在 offer
,则也会失败,而这种特性,则非常适合用来做高响应并且线程不固定的线程池的 Queue
。所以,在很多高性能服务器中,如果并发很高,这时候,普通的 LinkedQueue
就会成为瓶颈,性能就会出现毛刺,当换上 SynchronousQueue
后,性能就会好很多。
今天就看看这个特殊的 Queue 是怎么实现的。友情提示:代码有些小复杂。。。请做好心理准备。
源码实现
SynchronousQueue 内部分为公平(队列)和非公平(栈),队列的性能相对而言会好点。构造方法中,就看出来了。默认是非公平的,通常非公平(栈 FIFO)的性能会高那么一点点。
构造方法:
public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
offer 方法
该方法我们通常建议使用带有超时机制的 offer
方法。
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();
}
从上面的代码中,可以看到核心方法就是 transfer
方法。如果该方法返回 true
,表示,插入成功,如果失败,就返回 false
。
poll 方法
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();
}
同样的该方法也是调用了 transfer
方法。结果返回得到的值或者 null
。区别在于,offer
方法的 e
参数是实体的。而 poll
方法 e
参数是 null
,我们猜测,方法内部肯定根据这个做了判断。所以,重点在于transfer
方法的实现。
而 transferer 有 2 种,队列和栈,我们就研究一种,知晓其原理,另一种有时间在看。
TransferQueue 源码实现
构造方法:
TransferQueue() {
QNode h = new QNode(null, false); // initialize to dummy node.
head = h;
tail = h;
}
构造一个 Node 节点,注释说这是一个加的 node。并赋值给 head 和 tail 节点。形成一个初始化的链表。
看看这个 node:
/** Node class for TransferQueue. */
static final class QNode {
volatile QNode next; // next node in queue
volatile Object item; // CAS'ed to or from null
volatile Thread waiter; // to control park/unpark
final boolean isData;
}
node 持有队列中下一个 node,node 对应的值 value,持有该 node 的线程,拥有 park 或者 unpark,这里用的是 JUC 的工具类 LockSupport,还有一个布尔类型,isData,这个非常重要,需要好好理解,到后面我们会好好讲解。
我们更关注的是这个类的 transfer 方法,该方法是 SynchronousQueue 的核心。
该方法接口定义如下:
/**
* Performs a put or take. put 或者 take
*
* @param e if non-null, the item to be handed to a consumer;
* if null, requests that transfer return an item
* offered by producer.
* @param timed if this operation should timeout
* @param nanos the timeout, in nanoseconds
* @return if non-null, the item provided or received; if null,
* the operation failed due to timeout or interrupt --
* the caller can distinguish which of these occurred
* by checking Thread.interrupted.
*/
abstract E transfer(E e, boolean timed, long nanos);
注释说道 e 参数的作用:
如果 e 不是 null(说明是生产者调用) ,将 item 交给消费者,并返回 e;反之,如果是 null(说明是消费者调用),将生产者提供的 item 返回给消费者。
看看 TransferQueue 类的 transfer 方法实现,楼主写了很多的注释尝试解读:
QNode s = null; // constructed/reused as needed
boolean isData = (e != null);// 当输入的是数据时,isData 就是 ture,表明这个操作是一个输入数据的操作;同理,当调用者输入的是 null,则是在消费数据。
for (;;) {
QNode t = tail;
QNode h = head;
if (t == null || h == null) // 如果并发导致未"来得及"初始化
continue; // 自旋重来
// 以下分成两个部分进行
// 1. 如果当前操作和 tail 节点的操作是一样的;或者头尾相同(表明队列中啥都没有)。
if (h == t || t.isData == isData) {
QNode tn = t.next;
if (t != tail) // 如果 t 和 tail 不一样,说明,tail 被其他的线程改了,重来
continue;
if (tn != null) { // 如果 tail 的 next 不是空。就需要将 next 追加到 tail 后面了。
advanceTail(t, tn); // 使用 CAS 将 tail.next 变成 tail,
continue;
}
if (timed && nanos <= 0) // 时间到了,不等待,返回 null,插入失败,获取也是失败的。
return null;
if (s == null) // 如果能走到这里,说明 tail 的 next 是 null,这里的判断是避免重复创建 Qnode 对象。
s = new QNode(e, isData);// 创建一个新的节点。
if (!t.casNext(null, s)) // 尝试 CAS 将这个刚刚创建的节点追加到 tail 的 next 节点上.
continue;// 如果失败,则重来
advanceTail(t, s); // 当新的节点成功追加到 tail 节点的 next 上了, 就尝试将 tail.next 节点覆盖 tail 节点,称之为推进。
// s == 新节点,“可能”是新的 tail;e 是实际数据。
Object x = awaitFulfill(s, e, timed, nanos);// 该方法作用就是,让当前线程等待。排除意外情况和超时的话,就是等待其他线程拿走数据并替换成 isData 不同的数据。
if (x == s) { // x == s 是什么意思呢? 表明在 awaitFulfill 方法中,这个数据被取消了,tryCancel 方法就是将 item 覆盖了 QNode。说明这次操作失败了。
clean(t, s);// 操作失败则需要清理数据,并返回 null。
return null;
}
// 如果一切顺利,确实被其他线程唤醒了,其他线程也交换了数据。
// 这个判断:next != this,说明了什么?当这个 tail 节点的 next 不再指向自己,说明了
if (!s.isOffList()) { // not already unlinked
// 这一步是将 S 节点设置为 Head,并且将新 Head 的 next 指向自己,让 Head 和之前的 next 断开。
advanceHead(t, s); // unlink if head
// 当 x 不是 null,表明对方线程是存放数据的。
if (x != null) // and forget fields
// 这一步操作将自己的 item 设置成自己。
s.item = s;
// 将 S 节点的持有线程变成 null。
s.waiter = null;
}
// x 不是 null 表明,对方线程是生产者,返回他生产的数据;如果是 null,说明对方线程是消费者,那他自己就是生产者,返回自己的数据,表示成功。
return (x != null) ? (E)x : e;
}
// 2. 如果当前的操作类型和 tail 的操作不一样。称之为互补。
else { // complementary-mode
QNode m = h.next; // node to fulfill
// 如果下方这些判断没过,说明并发修改了,自旋重来。
if (t != tail || m == null || h != head)
continue; // inconsistent read
Object x = m.item;
// 如果 head 节点的 isData 和当前操作相同,
// 如果 操作不同,但 head 的 item 就是自身,也就是发生了取消操作,tryCancel 方法会做这件事情。
// 如果上面2个都不满足,尝试使用 CAS 将 e 覆盖 item。
if (isData == (x != null) || // m already fulfilled
x == m || // m cancelled
!m.casItem(x, e)) { // lost CAS
// CAS 失败了,Head 的操作类型和当前类型相同,item 被取消了,都会走这里。
// 将 h.next 覆盖 head。重来。
advanceHead(h, m); // dequeue and retry
continue;
}
// 这里也是将 h.next 覆盖 head。能够走到这里,说明,上面的 CAS 操作成功了,当前线程已经将 e 覆盖了 next 的 item 。
advanceHead(h, m); // successfully fulfilled
// 唤醒 next 的 线程。提醒他可以取出数据,或者“我”已经拿到数据了。
LockSupport.unpark(m.waiter);
// 如果 x 不是 null,表明这是一次消费数据的操作,反之,这是一次生产数据的操作。
return (x != null) ? (E)x : e;
}
}
说实话,代码还是比较复杂的。JDK 中注释是这么说的:
基本算法是死循环采取 2 种方式中的其中一种。
1 如果队列是空的,或者持有相同的模式节点(isData
相同),就尝试添加节点到队列中,并让当前线程等待。
2 如果队列中有线程在等待,那么就使用一种互补
的方式,使用 CAS 和等待者交换数据。并返回。
什么意思呢?
首先明确一点,队列中,数据有 2 种情况(但同时只存在一种),要么 QNode
中有实际数据(offer
的时候,是有数据的,但没有“人”来取),要么没有实际数据(poll
的时候,队列中没有数据,线程只好等待)。队列在哪一种状态取决于他为空后,第一个插入的是什么类型的数据
。
楼主画了点图来表示:
- 队列初始化的时候,只有一个空的
Node
。
- 此时,一个线程尝试
offer
或者poll
数据,都会插入一个Node
插入到节点中。
- 假设刚刚发生的是 offer 操作,这个时候,另一个线程也来 offer,这时就会有 2 个节点。
- 这个时候,队列中有 2 个有真实数据(offer 操作)的节点了,注意,这个时候,那 2 个线程都是
wait
的,因为没有人接受他们的数据。此时,又来一个线程,做 poll 操作。
从上图可以看出, poll
线程从 head
开始取数据,因为它的 isData
和 tail
节点的 isData 不同,那么就会从 head 开始找节点,并尝试将自己的 null 值和节点中的真实数据进行交换。并唤醒等待中的线程。
这 4 幅图就是 SynchronousQueue
的精华。
既然叫做同步队列,一定是 A 线程生产数据的时候,有 B 线程在消费,否则 A 线程就需要等待,反之,如果 A 线程准备消费数据,但队列中没有数据,线程也会等待,直到有 B 线程存放数据。
而 JDK 的实现原理则是:使用一个队列,队列中的用一个 isData
来区分生产还是消费,所有新操作都根据 tail 节点的模式来决定到底是追加到 tail
节点还是和 tail
节点(从 head
开始)交换数据。
而所谓的交换是从head
开始,取出节点的实际数据,然后使用 CAS
和匹配到的节点进行交换。从而完成两个线程直接交换数据的操作。
为什么他在某些情况下,比LinkedBlockingQueue
性能高呢?其中有个原因就是没有使用锁,减少了线程上下文切换。第二则是线程之间交换数据的方式更加的高效。
好,重点部分讲完了,再看看其中线程是如何等待的。逻辑在 awaitFulfill
方法中:
// 自旋或者等待,直到填充完毕
// 这里的策略是什么呢?如果自旋次数不够了,通常是 16 次,但还有超过 1 秒的时间,就阻塞等待被唤醒。
// 如果时间到了,就取消这次的入队行为。
// 返回的是 Node 本身
// s.item 就是 e
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) ?// 如果成功将 tail.next 覆盖了 tail,如果有超时机制,则自旋 32 次,如果没有超时机制,则自旋 32 *16 = 512次
(timed ? maxTimedSpins : maxUntimedSpins) : 0);
for (;;) {
if (w.isInterrupted())// 当前线程被中断
s.tryCancel(e);// 尝试取消这个 item
Object x = s.item;// 获取到这个 tail 的 item
if (x != e) // 如果不相等,说明 node 中的 item 取消了,返回这个 item。
// 这里是唯一停止循环的地方。当 s.item 已经不是当初的哪个 e 了,说明要么是时间到了被取消了,要么是线程中断被取消了。
// 当然,不仅仅只有这2种 “意外” 情况,还有一种情况是:当另一个线程拿走了这个数据,并修改了 item,也会通过这个判断,返回被“修改”过的 item。
return x;
if (timed) {// 如果有时间限制
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {// 如果时间到了
s.tryCancel(e);// 尝试取消 item,供上面的 x != e 判断
continue;// 重来
}
}
if (spins > 0)// 如果还有自旋次数
--spins;// 减一
else if (s.waiter == null)// 如果自旋不够,且 tail 的等待线程还没有赋值
s.waiter = w;// 当前线程赋值给 tail 的等待线程
else if (!timed)// 如果自旋不够,且如果线程赋值过了,且没有限制时间,则 wait,(危险操作)
LockSupport.park(this);
else if (nanos > spinForTimeoutThreshold)// 如果自旋不够,且如果限制了时间,且时间还剩余超过 1 秒,则 wait 剩余时间。
// 主要目的就是等待,等待其他线程唤醒这个节点所在的线程。
LockSupport.parkNanos(this, nanos);
}
}
该方法逻辑如下:
- 默认自旋 32 次,如果没有超时机制,则 512 次。
- 如果时间到了,或者线程被中断,则取消这次的操作,将
item
设置成自己。供后面判断。 - 如果自旋结束,且剩余时间还超过 1 秒,则阻塞等待至剩余时间。
- 当线程被其他的线程唤醒,说明数据被交换了。则
return
,返回的是交换后的数据。
总结
好了,关于 SynchronousQueue
的核心源码分析就到这里了,楼主没有分析这个类的所有源码,只研究了核心部分代码,这足够我们理解这个 Queue
的内部实现了。
总结下来就是:
JDK 使用了队列或者栈来实现公平或非公平模型。其中,isData
属性极为重要,标识这这个线程的这次操作,决定了他到底应该是追加到队列中,还是从队列中交换数据。
每个线程在没有遇到自己的另一半时,要么快速失败,要么进行阻塞,阻塞等待自己的另一半来,至于对方是给数据还是取数据,取决于她自己,如果她是消费者,那么他就是生产者。
good luck!!!!
并发编程之 SynchronousQueue 核心源码分析的更多相关文章
- SynchronousQueue核心源码分析
一.SynchronousQueue的介绍 SynchronousQueue是一个不存储元素的阻塞队列.每一个put操作必须等待一个take操作,否则不能继续添加元素.SynchronousQueue ...
- Java并发编程之CAS二源码追根溯源
Java并发编程之CAS二源码追根溯源 在上一篇文章中,我们知道了什么是CAS以及CAS的执行流程,在本篇文章中,我们将跟着源码一步一步的查看CAS最底层实现原理. 本篇是<凯哥(凯哥Java: ...
- iOS 开源库系列 Aspects核心源码分析---面向切面编程之疯狂的 Aspects
Aspects的源码学习,我学到的有几下几点 Objective-C Runtime 理解OC的消息分发机制 KVO中的指针交换技术 Block 在内存中的数据结构 const 的修饰区别 block ...
- HashMap的结构以及核心源码分析
摘要 对于Java开发人员来说,能够熟练地掌握java的集合类是必须的,本节想要跟大家共同学习一下JDK1.8中HashMap的底层实现与源码分析.HashMap是开发中使用频率最高的用于映射(键值对 ...
- Spark GraphX图计算核心源码分析【图构建器、顶点、边】
一.图构建器 GraphX提供了几种从RDD或磁盘上的顶点和边的集合构建图形的方法.默认情况下,没有图构建器会重新划分图的边:相反,边保留在默认分区中.Graph.groupEdges要求对图进行重新 ...
- jquery事件核心源码分析
我们从绑定事件开始,一步步往下看: 以jquery.1.8.3为例,平时通过jquery绑定事件最常用的是on方法,大概分为下面3种类型: $(target).on('click',function( ...
- 迷你版jQuery——zepto核心源码分析
前言 zepto号称迷你版jQuery,并且成为移动端dom操作库的首选 事实上zepto很多时候只是借用了jQuery的名气,保持了与其基本一致的API,其内部实现早已面目全非! 艾伦分析了jQue ...
- 并发编程之:AQS源码解析
大家好,我是小黑,一个在互联网苟且偷生的农民工. 在Java并发编程中,经常会用到锁,除了Synchronized这个JDK关键字以外,还有Lock接口下面的各种锁实现,如重入锁ReentrantLo ...
- FutureTask核心源码分析
本文主要介绍FutureTask中的核心方法,如果有错误,欢迎大家指出! 首先我们看一下在java中FutureTask的组织关系 我们看一下FutureTask中关键的成员变量以及其构造方法 //表 ...
随机推荐
- AFNetworking 3.0 AFHTTPSessionManager文件下载
#import "ViewController.h" #import <AFNetworking.h> @interface ViewController () - ( ...
- iOS开发 关于iBeacon的一些记录
最近时间一直在研究ibeacon所以把自己遇到的一些问题写下来做个笔记. 参考资料:https://github.com/nixzhu/dev-blog/blob/master/2014-04-23- ...
- 附加题:将四则运算源代码上传到Github账户上
1.创建仓库用于存储管理本地文件 2.远程添加github上的Blog仓库. 3.获取github中Blog仓库的地址. 4.在Add Remote窗口中填写名字.Location. 5.将本地文件通 ...
- linux下git远程仓库的搭建
一.服务器环境 ubuntukylin-16.04-server-amd64 二.远程服务器创建一个名字叫git的用户,专门用于管理git仓库. $ adduser git 三.安装git.服务器端和 ...
- min cost max flow算法示例
问题描述 给定g个group,n个id,n<=g.我们将为每个group分配一个id(各个group的id不同).但是每个group分配id需要付出不同的代价cost,需要求解最优的id分配方案 ...
- Modeless对话框如何响应快捷键
MFC,Modeless对话框不会响应快捷键.解决的方案很多,其中之一是在PreTranslateMessage中地键盘消息进行拦截处理.
- C# 读取资源文件.resx 中的xml资源
主要是以字符串的形式来读取xml,然后通过遍历读取节点,通过节点属性名称获取属性值 /// <summary> /// 初始化OPC参数配置 /// </summary> // ...
- 【javascript】原生js更改css样式的两种方式
下面我给大家介绍的是原生js更改CSS样式的两种方式: 1通过在javascript代码中的node.style.cssText="css表达式1:css表达式2:css表达式3 &quo ...
- API网关【gateway 】- 1
最近在公司进行API网关重写,公司内采用serverMesh进行服务注册,调用,这里结合之前学习对API网关服务进行简单的总结与分析. 网关的单节点场景: 网关的多节点场景: 这里的多节点是根据模块进 ...
- C#导出Excel文件Firefox中文件名乱码
首先说明下:我的解决方法不一定适用于其他遇到该问题的人,因为情况多种多样,适合我的方法不一定适合别人,就像我在遇到问题时查到别人的解决方案放到我的代码里却不管用,所以这个方法仅供参考 这两天做了一个导 ...