SynchronousQueue
是一个同步阻塞队列,它的每个插入操作都要等待其他线程相应的移除操作,反之亦然。SynchronousQueue 像是生产者和消费者的会合通道,它比较适合“切换”或“传递”这种场景:一个线程必须同步等待另外一个线程把相关数据传递给它。
不同于之前的 ArrayBlockingQueue,LinkedBlockingQueue…对于它们来说生产者线程将数据放入存储空间(数组,队列),消费者线程从空间中拿数据,双方彼此之间不交互,而 SynchronousQueue
则完全不同,其内部没有存储空间,无论是生产者线程还是消费者线程,都将其创建为一个节点,节点构成链,生产者节点去链中匹配消费者,反之亦然。双方通过这种方式来通信。
关于节点链: SynchronousQueue
提供了两种策略:公平与非公平(默认非公平模式)。公平策略下,能够保证等待最久的消费者线程能够优先拿到元素或者等待最久的生产者线程能够优先转交元素。非公平通过栈(LIFO)实现,公平通过队列(FIFO)实现,对应的类为TransferStack
与 TransferQueue
,各自节点类型 SNode
与 QNode
。队列通常用于支持更高的吞吐量,栈则支持更高的线程局部存储。
TransferStack
与 TransferQueue
都继承自 Transferer
,该接口只有一个方法abstract E transfer(E e, boolean timed, long nanos);
,无论是 取 或是 添加 操作都调用该方法,因为在 SynchronousQueue 中这两个动作都是一个意思,就是取匹配。
关于节点:
类的继承图:
源码解析
在之前分析的等待队列实现都会用到锁,而 SynchronousQueue 没有用到锁,来看看他的实现吧,会让你受益匪浅的。
构造器
public SynchronousQueue() {
this(false);
}
public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
默认非公平模式,创建 TransferStack
来操作链。
添加 与 取操作,如 poll, take, put…都调用了 transferer.transfer(…),意思是 匹配:生产者 匹配 消费者,消费者 匹配 生产者 ,添加 与 取 的语义都转化为了 匹配。
TransferStack
代码的目的是要实现功能,在介绍源码之前先来概述 TransferStack 实现的功能。之前说 取/加 操作都变为 匹配,生产者线程匹配消费者线程,消费者线程匹配生产者线程,我们以 SNode 节点作为每个请求操作的实体,节点之间构成 链,匹配就变成了节点去链中找寻节点,实现并没有用到锁,而是建立在 CAS 之上,也就是建立在失败重试的基础上,它确保了每一步的安全性,但是没有锁的排斥并发下多线程对链表同时操作,那又如何确保节点匹配的安全性 ?节点去链表中找寻另一个节点的过程叫匹配,只要保证并发下当有节点在执行匹配时,其它线程就不能改变链表,如果可以使用锁的话便可以锁住代码以排斥其它线程,不能使用锁我们就需要设计链表的操作规则以实现这种功能,如何实现我们从代码中找寻答案。
匹配的核心是 transfer 方法,在介绍它之前先将其用到的方法,变量进行讲解。
static final class TransferStack<E> extends Transferer<E> {
/** 等待匹配的消费者 */
static final int REQUEST = 0;
/** 等待匹配的生产者 */
static final int DATA = 1;
/** 正在匹配的节点 */
static final int FULFILLING = 2;
既然生产者消费者的节点为统一类型,如这里的 SNode
,那么节点就需要有身份标识,REQUEST 代表消费者,DATA 代表生产者,正在执行匹配逻辑的节点标识为 FULFILLING,一来是防止其它人匹配它,二来当其它线程检测到该状态的节点会去帮助其执行匹配操作。
static final class SNode {
volatile SNode next; // 堆中的下一个节点
volatile SNode match; // 指向匹配的节点
volatile Thread waiter; // 指定阻塞/唤醒的线程,在阻塞前会将该字段指向当前运行线程
// 为什么下面两个不用 volatile 修饰 ?
Object item; // 存储数据,对于消费者则为null
int mode; // 标识该节点身份/状态
// 二者的赋值发生在节点对象创建阶段,之后不会再对其进行更改。
// Note: item and mode fields don't need to be volatile
// since they are always written before, and read after,
// other volatile/atomic operations.
SNode(Object item) {
this.item = item;
}
// cas 将当前节点的next指向由 cmp 改为 val
boolean casNext(SNode cmp, SNode val) {
return cmp == next &&
UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
/**
* 这段代码的调用是这样的,假设当前正在匹配的节点 s ,
* 它首先会尝试匹配其 next 指向的节点,假设为 m ,
* 他会这么调用 m.tryMatch(s),将 m 节点的 match 指向自己,
* 即匹配,若成功则代表二者相匹配,从链表中删除二者。
* 这里有四点:1,m 可能在阻塞,所以需要判断是否需要唤醒,
* 判断依据便是其 waiter 字段是否为空。
* 2,头节点的匹配操作并非只有头节点一个线程在执行,
* 线程们创建自己的节点竞争压入栈顶,但当头节点正在执行匹配逻辑时,
* 想要压入栈顶的线程都会失败,转而去帮助头节点进行匹配操作。
* 这就是这里曹永cas更改match的原因。
* 匹配成功的线程是将 m 节点的 match 由 null 改为 xx 的线程
* 3,match不为null,可能是其它线程帮助完成了匹配操作,或者当前节点
* 处于删除状态,删除状态的节点其match指向自己。
* 4,最后 return match == s; 而不是直接返回false,并发下
* 执行匹配的线程中有一个成功怎么让其它所有的线程都知道,也就是返回true
*
* Tries to match node s to this node, if so, waking up thread.
* Fulfillers call tryMatch to identify their waiters.
* Waiters block until they have been matched.
*
* @param s the node to match
* @return true if successfully matched to s
*/
boolean tryMatch(SNode s) {
if (match == null &&
UNSAFE.compareAndSwapObject(this, matchOffset, null, s)) {
// 若是被匹配节点 m 是阻塞状态,则其 waiter 指向 m 的线程
// 这里获取它并唤醒。别唤醒后线程执行逻辑会回到 awaitFulfill 方法
// 之后回到 transfer 方法,下面会介绍它们。
Thread w = waiter;
if (w != null) {
// waiters need at most one unpark
waiter = null;
LockSupport.unpark(w);
}
return true;
}
return match == s;
}
/**
* 在超时,中断两种情况下我们会调用该方法,标识该节点为删除状态,
* 通过将 match 指向自身来标识,删除状态的节点将不被匹配,。
* 毕竟并发下情况复杂,并不能保证结果,
* 这也就要求你在实现中考虑到操作达不到预期结果时该如何处理,
* 这些你都可以在 transfer 方法中看到。
* Tries to cancel a wait by matching node to itself.
*/
void tryCancel() {
UNSAFE.compareAndSwapObject(this, matchOffset, null, this);
}
// 判断该节点是否已处于删除状态
boolean isCancelled() {
return match == this;
}
// Unsafe mechanics
......省略 Usafe 相关
}
接下来继续回到 TransferStack 中,来介绍它的方法。
// 代表栈顶,插入取出都在头部
volatile SNode head;
isFulfilling:判断节点是否正在执行匹配操作。
/**
* 首先先来介绍下节点创建时 mode 的设定:
* 当前线程要执行的操作与头节点相同,即 mode 相同,不去为其执行匹配操作,
* 创建节点,mode标识其身份 REQUEST 或 DATA,等待被其它节点匹配。
* 若是不同,我们将节点的 mode 置为 FULFILLING | mode,该节点会执行匹配逻辑
* 所以判断一个节点是否在执行匹配操作,只需让其 mode & FULFILLING,
* 不等于0则代表其正在执行匹配逻辑。
*/
static boolean isFulfilling(int m) {
return (m & FULFILLING) != 0; }
casHead:更改头节点
// 将头节点由 h 改为 nh
boolean casHead(SNode h, SNode nh) {
return h == head &&
UNSAFE.compareAndSwapObject(this, headOffset, h, nh);
}
snode:创建或更改节点。
/**
* 该方法很简单就是创建/更改节点,不过要理解其设计得先看其用处
* 该方法只在 transfer 方法中被调用,节点创建后利用 CAS 将节点压入堆顶
* 但并发下存在竞争,若是入栈失败,线程再次尝试仍会调用 snode 方法
* 此时节点已经创建不能重复创建,堆也可能发生变化,所以节点的 next 需要更新
* 之前说了节点是否执行匹配逻辑是根据此时堆顶节点情况,不同情况mode也会变化
* 所以 mode 也要更新
* Creates or resets fields of a node. Called only from transfer
* where the node to push on stack is lazily created and
* reused when possible to help reduce intervals between reads
* and CASes of head and to avoid surges of garbage when CASes
* to push nodes fail due to contention.
*/
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;
}
awaitFulfill:在阻塞前轮循多次。
链表中的节点分三种:正在执行匹配操作的节点,等待被匹配的节点,删除状态的节点。等待被匹配也就是阻塞,但在真正阻塞前会尝试多次循环,以这种方式来等待被匹配,毕竟线程的阻塞唤醒是有消耗的,而且并发下可能也只需等待很短的时间便会被匹配,这是种优化手段。
先来看看其他一些要用到的方法,变量:
shouldSpin:判断是否需要继续循环等待。awaitFulfill一开始会利用该方法来计算循环次数,在循环开始后每