基于JDK1.8详细介绍了SynchronousQueue的底层源码实现,包括“没有容量”的原理,以及“入队列“、“出队列”等操作源码。
文章目录
1 SynchronousQueue的概述
public class SynchronousQueue< E >
extends AbstractQueue < E >
implements BlockingQueue< E >, Serializable
SynchronousQueue来自于JDK1.5的JUC包,是一个支持并发操作的阻塞队列,相对于其他阻塞队列,SynchronousQueue不能简单的使用“有界”或者“无界”来形容,因为它的内部根本就没有容量,即不能像其他阻塞队列那样存储元素,非常有趣!
SynchronousQueue的每一个“入队”操作的线程必须等待一个“出队”操作的线程来接收,否则入队线程将会被阻塞;同理每一个“出队”操作的线程都必须等待一个“入队”操作的数据传递,这就是两个“匹配”的操作!两个匹配的操作是同步等待的,即一个先达到的操作必须等待另一个匹配的操作出现,两个操作才能成功匹配并传递数据之后返回,如果只有其中一个操作或者全都是一种操作,那么该操作将等待。
SynchronousQueue本身并不存储任何元素,它的作用更像是用来同步两个线程之间的数据传递,比如将一个线程中数据传递给另外一个线程使用,比如线程任务的传递。
默认采用非公平模式对等待的结点进行匹配(FILO),但是支持公平模式对等待的结点进行匹配(FIFO,构造器的fair参数设置为true即表示公平模式)。非公平模式可能造成部分线程请求长期阻塞,但是并发性能更高!
实现了Serializable接口,支持序列化;没有实现Cloneable接口,不支持克隆!
不支持null元素的传递!
2 SynchronousQueue的原理
2.1 主要属性
首先是一批常量,NCPUS、maxTimedSpins、maxUntimedSpins等用于阻塞时的自旋次数计算。spinForTimeoutThreshold 表示超时阻塞时间的最小值。
还有一个transferer属性,这是传输器实例的引用,用于在传输操作无法正常完成时使用结点表示存储阻塞的线程以及元素,初始化一个SynchronousQueue对象只会在内部初始化某一个公平或者非公平模式的子类实例。
/**
* CPU中通常一个内核一个线程,后来有了超线程技术,可以把一个物理核心,模拟成两个逻辑核心,线程量增加一倍
* 因此这里获取的是CPU的实际可用线程数量,比如 i7-8750H 它具有6核心12线程,因此获取的就是12而不是6
* 通常CPU的实际可用线程数量越高,运行并发的程序的效率也越高
*/
static final int NCPUS = Runtime.getRuntime().availableProcessors();
/**
* 超时等待的线程在阻塞之前应该自旋的次数
* 该值是经验推导的 ——它适用于各种处理器和 OS。从经验上看,最佳值似乎不会随 CPU 数量(超过 2)而变化,因此只是一个常数(0或32)。
*/
static final int maxTimedSpins = (NCPUS < 2) ? 0 : 32;
/**
* 非超时等待的线程在阻塞之前应该自旋的次数
* 通常大于超时等待的线程的旋转次数,因为不需要检查每次旋转的之后的剩余超时时间,它们旋转的更快(0或512)
*/
static final int maxUntimedSpins = maxTimedSpins * 16;
/**
* 采用自旋而不是使用park阻塞的超时时间边界纳秒数,这是也是一个估计值。
* 即超时时间大于1000L,那么使用parkNanos阻塞当前线程,否则采用快速的自旋等待即可
* 原因在于,非常短的超时等待无法做到十分精确,如果这时再进行超时等待,相反会让nanosTimeout的超时从整体上表现得反而不精确。
* 因此,在超时非常短的场景下,AQS会进入无条件的快速自旋而不是挂起线程。
* <p>
* 这个参数在AQS的超时获取锁,Condition的超时等待中也被使用
*/
static final long spinForTimeoutThreshold = 1000L;
/**
* 传输器实例的引用,用于在传输操作无法正常完成时存储阻塞的线程以及元素
* 只会初始化某一个公平或者非公平模式的子类实例
*/
private transient volatile Transferer<E> transferer;
/**
* 下面的字段只是为了兼容JDK1.5的SynchronousQueue的序列化策略
* 只有在序列化或者反序列化时才会初始化,在高版本的SynchronousQueue中永远不会使用
*/
private ReentrantLock qlock;
private WaitQueue waitingProducers;
private WaitQueue waitingConsumers;
2.2 主要内部类
主要内部类就是一个Transferer抽象内部类,以及两个子类TransferQueue、TransferStack,分别实现了非公平模式和公平模式的传输器,用于在传输操作无法正常完成时通过一个结点存储阻塞的线程以及元素,以及用于在后来的传输条件满足的时候对此前等待的结点进行匹配唤醒!
2.2.1 Transferer抽象传输器
Transferer表示抽象传输器,定义了子类TransferQueue、TransferStack的关键抽象同名方法transfer,外部类SynchronousQueue保存的传输器也是Transferer类型,这样可以避免定义两个属性。
根据源码注释,它的两个实现都是采用了Nonblocking Concurrent Objects with Condition Synchronization算法(http://www.cs.rochester.edu/u/scott/synchronization/pseudocode/duals.html)的改进版本。
/**
1. 抽象传输器
2. 定义了子类TransferQueue、TransferStack的抽象同名方法
3. 外部类SynchronousQueue保存的传输器也是Transferer类型,这样可以避免定义两个属性
*/
abstract static class Transferer<E> {
/**
* 传输数据,put或者take时都会调用该方法
*
* @param e 如果非空,则表示生产者要交给消费者的数据; 如果为null,则表示消费者请求获取生产者的数据
* @param timed 如果是超时操作,那么为true
* @param nanos 超时时间,以纳秒为单位
* @return 如果返回的结果非空,那么表示数据被转移了或获取了;如果为null,这个操作可能因为超时或者被中断而失败了。
*/
abstract E transfer(E e, boolean timed, long nanos);
}
2.2.2 TransferStack非公平传输器
基于Nonblocking Concurrent Objects with Condition Synchronization算法的Transferer的非公平模式的实现,使用LIFO顺序存取元素,简称为Dual Stack,直译就是双重栈!
底层是一个单链表的结构,结点类型是SNode类型,通过SNode的mode属性可以确定三种模式的结点:
- REQUEST:消费者结点,表示消费者在等待获取数据,mode= REQUEST(0)。
- DATA:生产者结点,表示生产者在等待传输数据,mode= DATA (1)。
- FULFILLING:匹配结点,表示某个请求模式的结点正在与栈中的另一个等待的结点完成匹配,mode= 该请求原本的mode | FULFILLING(2),即mode为2或者3。
TransferStack持有一个单链表头部head作为“栈顶”,调用无参构造器时什么也不做。
属性的赋值都是采用CAS操作来完成的,下面主要是属性和构造器的介绍,方法没有列举出来,部分注释可能比较晦涩,不过没关系,后面的方法源码部分会详细介绍!
/**
1. 基于Nonblocking Concurrent Objects with Condition Synchronization算法实现的Dual stack ,双重栈
*/
static final class TransferStack<E> extends Transferer<E> {
/*
* 扩展了 Scherer-Scott dual stack 算法,
* 使用"覆盖"结点而不是位标记指针:在标记结点上执行操作推送(设置了FULFILLING标志位)以保留一个点以匹配等待结点。
*/
/* 结点模式*/
/**
* 消费线程在等待获取数据
*/
static final int REQUEST = 0;
/**
* 生产线程在等待传输数据
*/
static final int DATA = 1;
/**
* 某个请求与stack中另一个未完成的结点请求(REQUEST/DATA)相匹配时
* 会构造一个mode为 FULFILLING | 该请求原本mode 的结点,即mode为2或3
*/
static final int FULFILLING = 2;
/**
* 栈的头部元素,即栈顶
*/
volatile SNode head;
/**
* TransferStack的结点内部类
*/
static final class SNode {
/**
* 后继
*/
volatile SNode next; // next node in stack
/**
* 匹配时指向匹配的结点,取消等待时指向自身。
*/
volatile SNode match; // the node matched to this
/**
* 当前结点所属的线程,用于控制 park/unpark
*/
volatile Thread waiter; // to control park/unpark
/**
* 数据域,REQUEST模式则为null
*/
Object item; // data; or null for REQUESTs
/**
* 模式
* REQUEST 或者 DATA 或者 FULFILLING | mode
*/
int mode;
/**
* 构造器
*
* @param item 数据
*/
SNode(Object item) {
this.item = item;
}
//CAS操作都是使用UNSAFE来完成的,下面是UNSAFE相关初始化操作
// 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);
}
}
}
//CAS操作都是使用UNSAFE来完成的,下面是UNSAFE相关初始化操作
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long headOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> k = TransferStack.class;
headOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("head"));
} catch (Exception e) {
throw new Error(e);
}
}
}
2.2.3 TransferQueue公平传输器
基于Nonblocking Concurrent Objects with Condition Synchronization算法的Transferer的公平模式的实现,使用FIFO顺序存取元素,简称为Dual Queue,直译就是双重队列!
底层是一个单链表的结构,结点类型是Qnode类型,一个队列保存了两种模式的结点,通过Qnode的isData属性确定:
- isData=true:表示是生产者结点;
- isData=false:表示是消费者结点。
TransferQueue持有一个单链表头部head和尾部tail,调用无参构造器时初始化一个item为null,idDate为false的哨兵结点,head和tail都指向该结点。head则始终都指向一个哨兵结点。当一个模式的请求和队列head后继结点的请求匹配完成时,表示这两个结点完成数据传输。
下面主要是属性和构造器的介绍,方法没有列举出来,部分注释可能比较晦涩,不过没关系,后面的方法源码部分会详细介绍!
/**
* 基于Nonblocking Concurrent Objects with Condition Synchronization算法实现的Dual Queue,双重队列
*/
static final class TransferQueue<E> extends Transferer<E> {
/*
* 扩展了 Scherer-Scott dual queue 算法,采用QNode内部的模式来代替标记指针
* 该算法的实现比 dual stack的实现跟简单,因为没有fulfillers模式,匹配结点通过
* CAS的设置QNode.item完成
*/
/**
* 队列头结点
*/
transient volatile QNode head;
/**
* 队列尾结点
*/
transient volatile QNode tail;
/**
* 中断或超时结点的前继结点,用于移除的结点属于尾结点时使用
*/
transient volatile QNode cleanMe;
/**
* TransferQueue的构造器
*/
TransferQueue() {
//初始化一个哨兵结点,item为null,isData为false
QNode h = new QNode(null, false); // initialize to dummy node.
//head和tail都指向该结点
head = h;
tail = h;
}
/**
* TransferQueue的结点内部类
*/
static final class QNode {
/**
* 后继
*/
volatile QNode next; // next node in queue
/**
* 数据域
*/
volatile Object item; // CAS'ed to or from null
/**
* 当前结点所属的线程,用于控制 park/unpark
*/
volatile Thread waiter; // to control park/unpark
/**
* 是否存放了数据 true 是 false 否
*/
final boolean isData;
/**
* QNode的构造器
*
* @param item 数据
* @param isData 是否存放数据
*/
QNode(Object item, boolean isData) {
this.item = item;
this.isData = isData;
}
//CAS操作都是使用UNSAFE来完成的,下面是UNSAFE相关初始化操作
// 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);
}
}
}
//CAS操作都是使用UNSAFE来完成的,下面是UNSAFE相关初始化操作
private static final sun.misc.Unsafe UNSAFE;
private static final long headOffset;
private static final long tailOffset;
private static final long cleanMeOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> k = TransferQueue.class;
headOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("head"));
tailOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("tail"));
cleanMeOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("cleanMe"));
} catch (Exception e) {
throw new Error(e);
}
}
}
2.3 构造器
2.3.1 SynchronousQueue()
public SynchronousQueue()
创建一个具有非公平访问策略的 SynchronousQueue。
/**
* 创建一个具有非公平访问策略的 SynchronousQueue。
*/
public SynchronousQueue() {
//内部调用另一个构造器,传入false
this(false);
}
2.3.2 SynchronousQueue(fair)
public SynchronousQueue(boolean fair)
创建一个具有指定公平策略的 SynchronousQueue。fair如果为 true,则等待线程以 FIFO 的顺序竞争访问;否则顺序是不可预期的。
/**
1. 创建一个具有指定公平策略的 SynchronousQueue
2. 3. @param fair true 公平策略,创建一个具有指定公平策略的 SynchronousQueue false 非公平策略
*/
public SynchronousQueue(boolean fair) {
//根据不同的模式创建不同的传输器实例,然后赋给transferer
//true -> TransferQueue
//false -> TransferStack
transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
2.4 非公平模式原理
非公平模式是指底层传输器实例为TransferStack类型,调用的方法也是TransferStack内部的方法!
head永远都指向null或者栈顶结点。新来的请求如果不能和栈顶结点匹配,会被构造成REQUEST或者DATA模式的结点压入栈顶等待;新来的请求如果可以和栈顶结点匹配,那么也会构造成一个FULFILLING结点压入栈顶,随后与下面的结点匹配,匹配成功两个结点一定是相邻的结点,成功之后都会出栈。
由于结点的入栈都是压入栈顶,这样的话TransferStack中等待的结点被匹配的优先级并不是入栈的先后顺序原则,而是越晚入栈的线程反而会被越先匹配,这就是非公平模式的由来。
2.4.1 transfer匹配结点
TransferStack中的transfer内部方法用于新请求去尝试匹配一个已存在的结点的请求,作为核心方法,其他内部方法都是在该方法内部被调用的。而开放给用户的外部方法比如put和take也都公用这一个方法,即同时用于传递、获取元素。
大概步骤就是,在一个for循环中尝试下面三种行为:
- 如果栈为空或者栈顶元素的模式与当前请求的模式一致,表示当前线程请求模式不能与栈顶结点的模式匹配。那么要么返回、要么等待:
- 如果是超时操作,并且此时剩余超时时间小于等于0。那么如果栈顶结点此时被取消了,那么尝试CAS的将head指向原head的后继,帮助移除,继续下一次循环;否则直接返回null。
- 如果不是超时操作,或者是超时操作但是剩余超时时间还大于0,表示还可以继续被动等待后来的结点匹配。如果等待过程中被匹配成功,则返回传递的数据;如果等待被取消了,那么返回null。
- 否则,就是栈顶元素的模式与当前请求的模式不一致,那么判断栈顶结点是否已经被匹配了(是否是匹配结点),如果没有匹配(不是匹配结点),表示当前请求可以主动与栈顶结点尝试匹配:
- 如果栈顶元素被取消了。那么尝试CAS的将head指向原head的后继,继续下一次循环;
- 如果栈顶元素没有被取消。尝试构建一个匹配结点,mode= 该请求的mode | FULFILLING(2),然后将匹配结点压入栈顶,原栈顶结点成为该结点的后继,表示该匹配结点尝试与后继结点匹配。随后内层循环进行匹配,成功之后两个匹配的结点一起弹出栈,并返回传递的数据;如果最终匹配失败,那么尝试将head指向null,,继续下一次外层循环;
- 否则,表示栈顶结点已经被匹配了(是匹配结点),那么当前线程尝试帮助栈顶结点和其后继结点完成后续的匹配-出栈过程,类似于第二种行为,之后继续下一次循环;
上面的大概步骤比较笼统,而且很多的细节都没有讲到,下面是详细步骤
- 初始化一个s变量为null,用于构建新结点或者后续重用结点。判断参数e是否为null,如果为null那么mode初始化为REQUEST(0),表示消费线程在请求获取数据;否则mode初始化为DATA(1),表示生产线程在请求传输数据。
- 开启一个死循环,尝试匹配结点:
- 获取此时的head赋给h,如果h为null,或者h不为null但是h的模式等于mode,即栈顶结点的模式和此线程的请求模式一致,表示当前线程请求模式不能与栈顶结点的模式匹配:
- 如果timed为true并且nanos小于等于0,说明是超时操作并且此时剩余超时时间小于等于0,即超时时间到了,不需要等待匹配了:
- 如果h不为null 并且 h结点已经被取消,那么尝试CAS的设置head从h指向h.next,弹出已被取消的结点可能会失败但是没关系,因为已被其他线程弹出然后继续下一次循环,尝试匹配新的head—h。
- 否则可能是h为null,即此时没有了阻塞的结点,栈为空,或者即h不为null,但是h也没有被取消,那么直接返回null,表示匹配失败,transfer方法结束。
- 否则说明不是超时操作,或者是超时操作但是剩余超时时间还大于0,表示还可以继续等待与后来的结点匹配。调用snode方法创建一个SNode结点(此前s为null)或者替换s结点数据(此前s不为null),然后尝试CAS的设置head从h指向最新s,将s压入栈顶,此时原栈顶结点h成为s的后继,s成为新栈顶结点。然后就是等待匹配了:
- 调用awaitFulfill方法用于结点s的自旋或者阻塞,直到s被匹配或者s被取消了,该方法才会返回,获取返回值m;
- 如果m==s 为true,即s的等待被取消了。那么调用clean方法从栈中移除s结点,然后返回null,表示匹配失败,transfer方法结束。
- 否则,s被后来的结点匹配了。重新获取此时最新的head赋值给h,如果h不为null,并且h.next等于s,说明s就是和h进行匹配的说明s就是和h进行匹配的,并且h中的线程还没有走到将两个结点出栈的那一步。那么尝试CAS的将head从h指向s.next,即s线程尝试帮助将已匹配的两个结点出栈。失败也没关系,那说明另外一个匹配线程完成了出栈操作。
- 判断s的mode如果是REQUEST,表示s是消费者,那么m就是生产者,返回m.item,否则,s是生产者,返回s.item。最终无论是take还是put都会返回它们之间传递的数据,表示匹配、传递数据成功,transfer方法结束。
- 如果timed为true并且nanos小于等于0,说明是超时操作并且此时剩余超时时间小于等于0,即超时时间到了,不需要等待匹配了:
- 否则,表示h不为null并且h的模式不等于mode,那么调用isFulfilling方法判断h是否被匹配;如果还没匹配,那么说明s可以与h尝试匹配:
- 继续判断h是否被取消了。如果被取消,那么调用casHead尝试CAS的将head从h指向nh,可能失败但没关系,因为说明其他线程完成了这一操作。然后继续下一次大循环;
- 如果没被取消。那么调用snode方法,创建一个SNode结点(此前s为null)或者替换s结点数据(此前s不为null),这里s的mode= FULFILLING | 该s请求原本mode,表示匹配结点模式。s.next=head,即h成为了s的后继,s成为新head结点,即栈顶结点。
- 然后调用casHead方法尝试CAS的将head从h指向nh,CAS失败则说明此时最新head变了,可能是有两个线程同时匹配一个等待的结点,其中一个CAS成功,那么另一个自然失败了,继续下一次外层循环;
- CAS成功则表示完成一部分s匹配h,然后开启一个循环,尝试h匹配s直到成功或者被匹配者取消:
- 获取s的后继m,第一次循环时m即上面CAS操作中的h,后续循环时m为新的后继,即m应该是s的match,但是不一定是,还需要下面的m校验。
- 如果此时m为null,如果此时m为null,表示m结点被取消了,并且由clean方法可知后面也没有等待的结点了或者循环到了栈底部,没有后继结点了。
- 尝试CAS的将head从s指向null,如果成功,表示没有等待结点了;否则,表示又有新的结点进来,head被改变了(新结点进来之后会发现h被匹配了,会尝试帮助h完成匹配,但是发现h的匹配结点m为null,因此会将head指向null,此时这里建立的s结点才会被真正清除。);
- 然后s也置为null,下一次使用snode新建结点,重新匹配。break结束内层for循环,继续下一次的外层主循环。
- 如果m不为null,获取m的后继mn。m调用tryMatch方法尝试匹配s,注意最上面是s匹配m,这里是m匹配s的操作。tryMatch会尝试将m.match设置为s:
- tryMatch成功之后,表示两个结点s和m彻底匹配成功。调用casHead,尝试CAS的将head从s指向mn,即将两个已被匹配的结点出栈,失败也没关系,那说明另外一个匹配线程完成了出栈操作。判断s的mode如果是REQUEST,表示s是消费者,那么m就是生产者,返回m.item,否则,s是生产者,返回s.item最终无论是take还是put都会返回它们之间传递的数据,表示匹配、传递数据成功。,transfer方法结束。
- tryMatch失败之后,表示m被取消了。尝试CAS的将s.next从m指向mn,即帮助移除m结点,继续下一次内层循环。下一次将会由新后继mn去匹配s,直到匹配成功;或者循环到了栈底部,此时后继mn为null,然后由于后继null,自然会设置head指向null,将可能进入第一大步。
- 否则,表示h被匹配了,那么尝试帮助后续匹配的步骤,这里和第二大步有些相似:
- 获取h的后继m,m应该是h的match,但是不一定是,还需要下面的m校验。
- 如果此时m为null,表示m结点被取消了,并且由clean方法可知后面也没有等待的结点了,或者上一次的第二大步的内层循环到了栈底部,没有后继结点了,后续的外层循环遇到了。
- 调用casHead,尝试CAS的将head从h指向null,如果成功,表示没有等待结点了,这个h结点也被清理了;如果失败,表示有另一个新的线程进来,并且另一个线程的这一步的CAS操作成功了。因此失败也没关系,无论成功还是失败,继续下一次外层循环。
- 如果m不为null,获取m的后继mn。类似于第二大步,帮助m调用tryMatch尝试匹配s,简单的说就是 tryMatch会尝试将m.match设置为s:
- tryMatch成功之后,表示两个结点h和m彻底匹配成功。调用casHead,尝试CAS的将head从h指向mn,即将两个已被匹配的结点出栈,失败也没关系,那说明另外一个匹配线程完成了出栈操作,继续下一次外层循环。
- tryMatch失败之后,表示m被取消了。尝试CAS的将h.next从m指向mn,即帮助移除m结点,继续下一次外层循环。下一次将会由新后继mn去匹配s,直到匹配成功;或者循环到了栈底部,此时后继mn为null,然后由于后继null,自然会设置head指向null,将可能进入第一大步。
- 获取此时的head赋给h,如果h为null,或者h不为null但是h的模式等于mode,即栈顶结点的模式和此线程的请求模式一致,表示当前线程请求模式不能与栈顶结点的模式匹配:
经过上面的步骤,我们发现如果两个结点相互匹配成功,那肯定是put和take请求相互匹配,并且那么最终两个transfer方法都会返回传输的数据。
一次比较完整的匹配过程如下:
- 新来的一个请求,如果发现此时栈顶结点和自己的模式不一样并且没有被匹配,那么构造一个FULFILLING结点s并压入为栈顶,这表示当前请求此时可能找到了一个匹配结点,实际上它总是与它的后继结点匹配。但是还没完,这相当于前驱结点确认了,它还必须要等待某个后继结点确定,这样才算是两个结点真正的匹配成功!
- 然后就是从它的后继开始,由后继结点m调用tryMatch尝试与s结点匹配,即使尝试将m.match设置为s。匹配成功之后,表示双方都确认了,将会唤醒m结点内部等待的线程,随后m与s结点双双出栈,然后m和s对应的请求都将会返回传递的数据item;如果匹配失败,那表示m结点被取消了,随后帮助移除m结点,并将s的后继设置为m的后继mn,下一次循环时将由mn调用tryMatch尝试与s结点匹配。
- 最终可能s的后面的结点都被取消了,此时s的后继为null,这时实际上没有能够匹配s的后继结点了,那么尝试将head设置为null,即将全部结点出栈,s结点也被废弃,随后进入下一次大循环,该请求可能会由于没有匹配的结点而构造为一个REQUEST或者DATA结点进入栈中等待!
- 现在该结点(请求)只有等待后面新来的请求主动与该它匹配,或者等待超时了或者等待被中断了,才能返回!而如果某个结点,因为超时时间到了还没有匹配成功,或者在等待匹配过程中被中断了,那么该结点对应的请求(线程)将会返回null。
/**
* 位于TransferStack中的方法
* 传递、获取一个元素
*
* @param e 如果非空,则表示生产者要交给消费者的数据; 如果为null,则表示消费者请求获取生产者的数据
* @param timed 如果是超时操作,那么为true
* @param nanos 超时时间,以纳秒为单位
* @return 如果返回的结果不为null,那么表示数据被转移了或获取了;如果为null,这个操作可能因为超时或者被中断而失败了。
*/
@SuppressWarnings("unchecked")
E transfer(E e, boolean timed, long nanos) {
//初始化一个s变量为null,用于构建新结点或者重用结点
SNode s = null; // constructed/reused as needed
//判断e是否为null
//如果为null那么mode初始化为REQUEST(0),表示消费线程在等待获取数据
//否则mode初始化为DATA(1),表示生产线程在等待传输数据
int mode = (e == null) ? REQUEST : DATA;
/*开启一个死循环,尝试匹配结点*/
for (; ; ) {
//获取此时的head赋给h
SNode h = head;
/*
* 1 如果h为null 或者 h不为null但是h的模式等于mode,即栈顶结点的模式和此线程的请求模式一致,
* 表示当前线程请求模式不能与栈顶结点的模式匹配
*/
if (h == null || h.mode == mode) { // empty or same-mode
/*
* 1.1 如果timed为true 并且 nanos小于等于0
* 说明是超时操作,并且此时剩余超时时间小于等于0,即超时时间到了,不需要等待匹配了
*/
if (timed && nanos <= 0) { // can't wait
/*1.1.1 如果h不为null,并且h已经被取消*/
if (h != null && h.isCancelled())
//那么尝试CAS的设置head从h指向h.next,弹出已被取消的结点
//可能会失败但是没关系,因为已被其他线程弹出
//然后继续下一次循环,尝试匹配新的head
casHead(h, h.next); // pop cancelled node
/*
* 否则,可能是:
* 1 h为null,即此时没有了阻塞的结点
* 2 h不为null,但是h没有被取消
* 以上两种情况满足任意一种,表示超时时间到了,都直接可以返回null了
*/
else
//直接返回null,transfer方法结束
return null;
}
/*
* 否则,说明不是超时操作,或者 是超时操作但是剩余超时时间还大于0,表示还可以继续等待后来的结点匹配。
*
* 调用snode方法创建一个SNode结点(此前s为null)或者替换s结点数据(此前s不为null),
* 然后尝试CAS的设置head从h指向最新s,将s压入栈顶,此时原栈顶结点h成为s的后继,s成为新栈顶结点。
* 然后就是等待匹配了
*/
else if (casHead(h, s = snode(s, e, h, mode))) {
/*
* 调用awaitFulfill方法用于结点s的自旋或者阻塞,直到s被匹配,或者s被取消了
*/
SNode m = awaitFulfill(s, timed, nanos);
/*
* awaitFulfill方法返回的m,
* 有可能是其他结点,即s匹配的结点
* 也有可能是s自己,即因为超时时间到了或者等待被中断而被取消了
*/
//如果m==s 为true,即s被取消了
if (m == s) { // wait was cancelled
//那么从栈中清除s结点
clean(s);
//返回null
return null;
}
/*
* 到这里,表示s被后来的结点匹配了。h赋值为最新的head,如果h不为null,并且h.next等于s,
* 说明s就是和h进行匹配的,并且h中的线程还没有走到将两个结点出栈的那一步
*/
if ((h = head) != null && h.next == s)
//尝试CAS的将head从h指向s.next,即s线程尝试帮助将已匹配的两个结点出栈
//失败也没关系,那说明另外一个匹配线程完成了出栈操作
casHead(h, s.next); // help s's fulfiller
/*
* 判断s的mode如果是REQUEST,表示s是消费者,那么m就是生产者,返回m.item,否则,s是生产者,返回s.item
* 最终无论是take还是put都会返回它们之间传递的数据,表示匹配、传递数据成功。
*/
return (E) ((mode == REQUEST) ? m.item : s.item);
}
}
/*
* 2 否则 表示h不为null并且h的模式不等于mode,那么调用isFulfilling判断h是否被匹配
* 如果还没匹配,那么说明s可以与h尝试匹配
*/
else if (!isFulfilling(h.mode)) { // try to fulfill
/*继续判断h是否被取消了*/
if (h.isCancelled()) // already cancelled
//如果被取消,那么尝试CAS的将head从h指向nh,帮助移除被取消的h,可能失败,没关系
//然后继续下一次大循环
casHead(h, h.next); // pop and retry
/*
* 如果没被取消
* 那么首先调用snode方法,创建一个SNode结点(此前s为null)或者替换s结点数据(此前s不为null),
* 这里s的mode= FULFILLING | 该s请求原本mode,表示匹配结点模式。
* s.next=head,即h成为了s的后继,s成为新head结点,即栈顶结点。
* 然后调用casHead尝试CAS的将head从h指向nh,CAS失败则说明此时最新head变了,
* 可能是有两个线程同时匹配一个等待的结点,其中一个CAS成功,那么另一个自然失败了,继续下一次循环
*/
else if (casHead(h, s = snode(s, e, h, FULFILLING | mode))) {
//CAS成功之后,表示s匹配h成功,开启一个循环,尝试h匹配s直到成功或者被匹配者取消
for (; ; ) { // loop until matched or waiters disappear
//获取s的后继m,第一次循环时m即上面CAS操作中的h,后续循环时m为新的后继,即m应该是s的match
//但是不一定是,还需要下面的m校验。
SNode m = s.next; // m is s's match
/*
* 如果此时m为null,表示m结点被取消了,并且由clean方法可知后面也没有等待的结点了
* 或者循环到了栈底部,没有后继结点了
*/
if (m == null) { // all waiters are gone
/*
* 尝试CAS的将head从s指向null,如果成功,表示没有等待结点了;
* 否则,表示又有新的结点进来,head被改变了。新结点进来之后会发现h被匹配了,
* 会尝试帮助h完成匹配,但是发现h的匹配结点m为null,因此会将head指向null,此时这里建立的s结点才会被真正清除
*/
casHead(s, null); // pop fulfill node
//s也置为null,下一次使用snode新建结点,重新匹配,目的是可以使得这个s结点被回收
s = null; // use new node next time
//结束内层for循环,继续下一次的外层主循环,将可能进入第一大步
break; // restart main loop
}
//如果m不为null,获取m的后继mn
SNode mn = m.next;
/*
* m调用tryMatch尝试匹配s,注意最上面是s匹配m,这里是m匹配s的操作
* 简单的说就是 tryMatch会尝试将m.match设置为s
*/
if (m.tryMatch(s)) {
//到这里的表示,两个结点彻底匹配成功
/*
* 成功之后,尝试CAS的将head从s指向mn,即移除两个已被匹配的结点
* 失败也没关系,那说明另外一个匹配线程完成了出栈操作
*/
casHead(s, mn); // pop both s and m
/*
* 判断s的mode如果是REQUEST,表示s是消费者,那么m就是生产者,返回m.item,否则,s是生产者,返回s.item
* 最终无论是take还是put都会返回它们之间传递的数据,表示匹配、传递数据成功。
*/
return (E) ((mode == REQUEST) ? m.item : s.item);
} else // lost match
/*
* 匹配失败,表示m被取消了。
* 尝试CAS的将s.next从m指向mn,即帮助移除m结点,继续下一次内层循环
* 下一次将会由新后继mn去匹配s,直到匹配成功,或者循环到了栈底部,此时后继mn为null
* 然后由于后继null,自然会设置head指向null,那么这个请求结点将进入第二大步
*/
s.casNext(m, mn); // help unlink
}
}
}
/*
* 3 否则,表示h被匹配了,那么帮助后续匹配的步骤
*/
else { // help a fulfiller
//获取h的后继m,m应该是h的match
SNode m = h.next; // m is h's match
/*
* 如果此时m为null,表示m结点被取消了,并且由clean方法可知后面也没有等待的结点了
* 或者 上一次的第二大步的内层循环到了栈底部,没有后继结点了,后续的外层循环遇到了
*/
if (m == null) // waiter is gone
/*
* 尝试CAS的将head从h指向null,如果成功,表示没有等待结点了,这个h结点也被清理了;
* 如果失败,表示又有另一个新的结点进来,并且另一个线程这一步的CAS操作成功了
* 因此失败也没关系,无论成功还是失败,继续下一次大循环。
*/
casHead(h, null); // pop fulfilling node
/*如果m不为null*/
else {
//获取m的后继mn
SNode mn = m.next;
/*
* m调用tryMatch尝试匹配s,简单的说就是 tryMatch会尝试将m.match设置为s
*/
if (m.tryMatch(h)) // help match
//到这里的表示,两个结点彻底匹配成功
/*
* 成功之后,尝试CAS的将head从h指向mn,即移除两个已被匹配的结点
* 失败也没关系,那说明另外一个匹配线程完成了出栈操作,继续下一次大循环。
*/
casHead(h, mn); // pop both h and m
else // lost match
/*
* 匹配失败,表示m被取消了。
* 尝试CAS的将h.next从m指向mn,即帮助移除m结点,继续下一次内层循环
* 下一次将会由新后继mn去匹配h,直到匹配成功,或者循环到了栈底部,此时后继mn为null
* 然后由于后继null,自然会设置head指向null,将可能进入第一大步
*/
h.casNext(m, mn); // help unlink
}
}
}
}
2.4.2 isCancelled结点是否取消
isCancelled方法是位于SNode结点类中的方法,由某个Snode结点调用,用于判断调用结点是否被取消了。
想要判断是否被取消很简单,就是看该结点的match属性是否指向自己,什么时候才会指向自己呢?实际上在awaitFulfill方法中,即等待被匹配的时候,如果等待过程中(自旋或者阻塞)线程被中断了或者由于是超时请求并且超时时间到了也没有匹配成功。这两种情况下都会调用tryCancel方法将等待结点的match属性指向自己,表示等待被取消。
被取消的结点将会被移除栈,并且结点对应的请求的transfer方法将会返回null。
/**
1. 位于SNode结点类中的方法
2. 3. @return 调用结点是否被取消,true 是 false 否
*/
boolean isCancelled() {
return match == this;
}
2.4.3 CAS操作
TransferStack中对于引用属性的赋值基本都是使用的CAS操作,这样能够保证单个变量符合操作的原子性,同时避免了锁的使用,提升了性能。
常用的有三个CAS方法:
- casHead,位于TransferStack中的方法,尝试CAS的改变栈顶head属性的指向,从h指向nh;
- casNext,位于SNode中的方法,尝试CAS的改变结点next属性的指向,从cmp指向val;
- tryCancel,位于SNode中的方法,尝试CAS的改变结点match属性的指向,从null指向自己,这就是尝试取消结点的方法;
/**
* 位于TransferStack中的方法
* 尝试CAS的将head从h指向nh
*
* @param h 预期head
* @param nh 新head
* @return true CAS成功; false CAS失败
*/
boolean casHead(SNode h, SNode nh) {
//如果h此时还是等于head 并且 将head从h指向nh成功,那么返回true;否则返回false
return h == head &&
UNSAFE.compareAndSwapObject(this, headOffset, h, nh);
}
/**
* 位于SNode中的方法
* 尝试CAS的将当前结点的next从cmp指向val
*
* @param cmp 预期后继
* @param val 新后继
* @return true CAS成功; false CAS失败
*/
boolean casNext(SNode cmp, SNode val) {
//如果此时cmp还是等于next 并且 将next从cmp指向val成功,那么返回true;否则返回false
return cmp == next &&
UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
/**
1. 位于SNode中的方法
2. 尝试取消结点
3. 很简单,就是尝试CAS的将该结点的match属性从null指向自己
*/
void tryCancel() {
UNSAFE.compareAndSwapObject(this, matchOffset, null, this);
}
2.4.4 snode创建结点/重置数据
snode是位于TransferStack中的方法,用于创建一个新结点或者重置老结点的数据。在transer方法中的两个地方被调用到:
- 进入第一个大步骤中,表示还可以继续等待后来的结点匹配的时候,将会调用snode方法返回一个SNode结点,并尝试入栈,等待后来的结点匹配;
- 进入第二个大步骤中,栈顶结点没被取消,此时说明s可以与h尝试匹配,将会调用snode方法返回一个SNode结点,并尝试入栈,尝试与后继的结点匹配;
为什么不是直接新建结点而是可能会重置结点属性呢? 因为随着我们调用snode返回一个s结点,后续会伴随着一个casHead操作,会尝试将s结点入栈并称为栈顶结点,但是如果线程竞争比较激烈,一个线程可能会失败多次。在第一次snode创建新结点之后,如果CAS失败后续调用的snode方法就可以复用第一次创建的s对象,而不必每次都创建SNode对象,从而节约空间。
/**
1. 位于TransferStack中的方法
2. 用于创建一个SNode结点或重置一个SNode结点的字段
3. 4. @param s 结点的引用,如果为null表示创建,否则表示重置
5. @param e 数据域
6. @param next 后继
7. @param mode 模式
8. @return 返回s,可能是新创建的结点也可能是重置了属性的s结点
*/
static SNode snode(SNode s, Object e, SNode next, int mode) {
//如果s为null,那么新建结点,item 存储 e,赋值给s
if (s == null) s = new SNode(e);
//设置mode
s.mode = mode;
//设置next
s.next = next;
//返回s
return s;
}
2.4.5 awaitFulfill等待匹配
awaitFulfill是位于TransferStack中的方法,如果栈顶结点和当前请求模式一致,并且当某个结点的请求不是超时操作或者是超时操作但是剩余超时时间还大于0,表示还可以继续等待后来的结点匹配,那么该结点的线程将会调用awaitFulfill方法。用于等待该结点被后来的请求匹配,将会返回被匹配的结点,或者在等待匹配的过程中被取消,将会返回该结点自己。
这个方法也很重要且难以理解,大概步骤为:
- 判断是否是超时操作,如果是,那么计算超时截止的时间点的纳秒值;否则为0。使用deadline保存。获取当前结点s的线程w。
- spins表示自旋次数,这里计算自旋次数。调用shouldSpin(s)方法判断在s结点的线程阻塞之前是否需要先自旋。如果需要,那么继续判断是否是超时操作,如果是,那么spins=maxTimedSpins;否则spins=maxUntimedSpins。如果不需要,那么spins=0,即自旋次数为0。
- 开启一个死循环,直到被匹配或者取消等待才能返回:
- 首先,判断的是w线程是否被中断了。如果被中断了,那么尝试调用tryCancel方法CAS的直接取消s结点,即s.match指向s自己,失败也没关系,这里对于match属性的CAS竞争主要是在tryMatch方法和tryCancel方法之间,无论它们谁CAS成功,match都将不会为null,因此在下面的判断中将会返回。
- 判断s.match的值m是否不为null,如果是,直接返回m,awaitFulfill方法结束。如果s被匹配了,s.match指向匹配的结点,或者如果s被取消了,s.match指向s自己,这两种情况match都不为null,都可以返回。
- 判断是否是超时操作,如果是,那么计算剩余超时时间纳秒nanos,如果nanos小于等于0,表示超时时间到了,但是还没有被匹配,那么同样尝试调用tryCancel方法CAS的直接取消s结点,即s.match指向s自己,随后continue结束本次循环,继续下一次循环,在下一次循环中将会因为s.match不为null而返回。
- 到这里,说明s没有被中断,并且s也没有被匹配,并且不是超时操作或者是超时操作但是时间还没用完。但这都是瞬时的状态,随时都可能改变。
- 首先判断spins即自旋次数是否大于0。如果大于0,那么再次调用shouldSpin方法判断s是否应该继续自旋,如果应该,那么spins=spins - 1;如果不应该,那么spins=0;然后结束本次循环,继续下一次循环。
- 否则,表示spins<=0,即自旋完毕还没有返回,表示可以阻塞了。判断如果s.waiter等于null,那么s.waiter = w,然后结束本次循环,继续下一次循环。
- 否则,表示自旋完毕可以阻塞了(spins小于等于0),并且阻塞之前的检查也做完了(s.waiter 不为 null),没有达到可返回的条件。判断如果不是超时操作,那么调用LockSupport.park(this)阻塞当前线程,即w线程,等待被唤醒。
- 否则,表示是超时操作,判断如果nanos大于spinForTimeoutThreshold(1000),那么调用LockSupport.parkNanos(this, nanos)阻塞当前线程(即w线程)nanos纳秒,等待被唤醒或者超时时间到。否则,继续下一次循环,即一直自旋直到超时或者可以返回为止。
这里有一些细节:
- 实际上在等待过程中,通常都是先自旋一定次数等待,如果此期间被匹配了或者取消了,那么结点对应的线程也就不必阻塞了。否则,自旋完毕还没有被匹配或者取消,那么对象对应的线程将会阻塞。为什么要先自旋呢?因为这里的阻塞、唤醒操作需要依赖底层系统的mutex互斥量,造成线程在用户状态和内核状态之间的切换,从而浪费时间,在s已被匹配或者s位于栈顶的情况下,大概率有新的请求进来并且能够该结点进行匹配。因此使用先自旋一定次数来代替直接阻塞,如果在自旋期间被匹配了,那么自旋花费的时间肯定远远小于线程阻塞、唤醒需要的时间,有利于提升程序的执行效率。
- 在第一个if判断中,即使spins自旋次数初始化为0或者变成了0(可能因为自旋完毕或者shouldSpin判断不需要自旋)。s也会进入for循环走一遍流程,检查是否达到了返回的条件。因为在刚进入awaitFulfill方法的时候,就有可能被匹配了、中断了、超时了,此时不需要阻塞。即spins=n,那么上面的判断将会走n+1次。
- 在第二个elsf if判断中,在第一次spins小于等于0的时候,表示自旋完毕也没有返回,可以阻塞了。但是这里并没有直接阻塞,还会判断如果s.waiter等于null,那么s.waiter = w,然后结束本次循环,继续下一次循环。从这里我们能看出,即使自旋完毕可以阻塞了,在真正的阻塞之前,还会至少再循环一次检查是否达到了返回的条件,因为状态是随时都可能会改变的:
- 实际上,如果这里将waiter置为w,如果有一个新结点进来,刚好正在与该结点匹配,那么在tryMatch方法中,会设置match的值,在下一次循环中,也可能会因为match不为null而退出循环而不阻塞。
- 另外即使w线程最终阻塞了,在tryMatch方法中设置了match的值之后,会判断waiter如果不为null,然后将waiter置为null,然后直接LockSupport.unpark(w)先唤醒w线程,这样w在被唤醒之后的下一个循环中将会由于满足条件而退出循环。
- 而在最坏的情况下:unpark方法先于park方法调用,此时由于LockSupport内部“许可”的原理,此前unpark给予一个许可,后续park方法也将会直接返回而不会阻塞,在下一个循环中同样将会由于满足条件而退出循环!
- 在超时操作的调用中,如果剩余超时时间纳秒值小于等于1000,那么改为一直自旋直到超时或者可以返回为止。因为Java中超时阻塞线程的时候方法会在线程状态切换以及底层mutex资源申请、释放时耗费一定时间,如果超时时间较短,那么短时间的超时等待是不精确的,还不如一直自旋更加精确。
/**
* 位于TransferStack中的方法
* 用于结点的自旋或者阻塞,直到被匹配或者取消等待
* <p>
* 如果栈顶结点和当前请求模式一致,并且当某个结点的请求不是超时操作或者是超时操作但是剩余超时时间还大于0,
* 表示还可以继续等待后来的结点匹配,那么该结点的线程将会调用awaitFulfill方法。
*
* @param s 自旋或者阻塞的结点
* @param timed 如果是超时操作,那么为true
* @param nanos 超时时间,纳秒
* @return s结点匹配成功就返回匹配的结点,或者s结点被取消了就返回s自己
*/
SNode awaitFulfill(SNode s, boolean timed, long nanos) {
//判断是否是超时操作,如果是,那么计算超时截止的时间点的纳秒值;否则为0,使用deadline保存
final long deadline = timed ? System.nanoTime() + nanos : 0L;
//获取当前结点s的线程w
Thread w = Thread.currentThread();
/*
* spins表示自旋次数,这里计算自旋次数
* 首先调用shouldSpin(s)方法判断在s结点的线程阻塞之前是否需要先自旋
* 如果需要,那么继续判断是否是超时操作:
* 如果是,那么spins=maxTimedSpins,即超时等待的线程在阻塞之前应该自旋的次数,如果CPU可用线程数量小于2,那么自旋没有太大意义,因此自旋次数为0
* 否则spins=maxUntimedSpins,即非超时等待的线程在阻塞之前应该自旋的次数,如果CPU可用线程数量小于2,那么自旋没有太大意义,因此自旋次数为0
* 如果不需要,那么spins=0,即自旋次数为0
*/
int spins = (shouldSpin(s) ?
(timed ? maxTimedSpins : maxUntimedSpins) : 0);
/*死循环,直到被匹配或者取消才能返回*/
for (; ; ) {
//首先是一系列是否满足返回的条件的判断
/*首先 判断的是w线程是否被中断了*/
if (w.isInterrupted())
/*
* 如果被中断了,那么尝试CAS的直接取消s结点,即s.match指向s自己
* 失败也没关系,这里对于match属性的CAS竞争主要是在tryMatch方法和tryCancel方法之间,
* 无论它们谁CAS成功,match都将不会为null,因此在下面的判断中将会返回
*/
s.tryCancel();
/*
* 然后 判断s.match是否不为null
* 如果s被匹配了,s.match指向匹配的结点
* 如果s被取消了,s.match指向s自己
* 这两种情况match都不为null,都可以返回。
*/
//获取s的匹配结点m,因为在上面的步骤中s就可能与其他结点匹配了
SNode m = s.match;
//如果m不为null,说明s已经匹配了
if (m != null)
//直接返回m,不需要自旋和阻塞,这是这个死循环唯一的出口
return m;
/*然后 判断是否是超时操作*/
//如果是超时操作
if (timed) {
//计算剩余超时时间纳秒nanos
nanos = deadline - System.nanoTime();
//如果nanos小于等于0,表示超时时间到了,但是还没有被匹配
if (nanos <= 0L) {
/*那么尝试直接取消s结点,s.match指向s自己*/
s.tryCancel();
//结束本次循环,继续下一次循环,在下一次循环中将会因为s.match不为null而返回
continue;
}
}
//然后是一系列是否继续自旋还是阻塞的判断
/*
* 到这里,说明:
* s没有被中断,并且s没有被匹配,并且不是超时操作或者是超时操作但是时间还没用完
*
* 首先判断spins即自旋次数是否大于0
*/
if (spins > 0)
/*
* 如果大于0,那么再次调用shouldSpin方法,这里的shouldSpin是判断s是否应该继续自旋
* 如果应该,那么spins=spins - 1;如果不应该,那么spins=0;然后结束本次循环,继续下一次循环
* 从这里我们能看出,即使spins自旋次数初始化为0或者变成了0(可能因为自旋完毕或者shouldSpin判断不需要自旋)
* s也会进入for循环走一遍流程,检查是否达到了返回的条件。
* 因为在刚进入awaitFulfill方法的时候,就有可能被唤醒了或者中断了,此时检查一遍就可以不需要阻塞。
* 即,spins=n,那么上面的判断将会走n+1次
*/
spins = shouldSpin(s) ? (spins - 1) : 0;
/*
* 到这里,表示spins<=0,即自旋完毕还没有返回,表示可以阻塞了
* 判断如果s.waiter等于null,那么s.waiter = w,然后结束本次循环,继续下一次循环
* 从这里我们能看出,即使自旋完毕可以阻塞了,在真正的阻塞之前,还会至少再循环一次检查是否达到了返回的条件。
*
* 实际上,如果这里将waiter置为w,在下一次循环中,如果有一个新结点进来,刚好正在与该结点匹配,那么在tryMatch方法中,
* 会设置match的值,这样在额外的一次循环中也可能会退出循环而不阻塞。
*
* 另外即使w线程最终阻塞了,在tryMatch方法中设置了match的值之后,会判断waiter如果不为null,然后将waiter置为null,
* 然后直接LockSupport.unpark(w)唤醒w线程,这样w在被唤醒之后的下一个循环中将会由于满足条件而退出循环
*
* 而在最坏的情况下:unpark先于park调用,此时由于LockSupport内部“许可”的原理,
* 此前unpark给予一个许可,park方法也将会直接返回而不会阻塞,在下一个循环中同样将会由于满足条件而退出循环
*/
else if (s.waiter == null)
s.waiter = w; // establish waiter so can park next iter
/*
* 到这里,表示自旋完毕可以阻塞了(spins小于等于0),并且阻塞之前的检查也做完了(s.waiter 不为 null),没有达到可返回的条件
* 判断如果不是超时操作,那么调用LockSupport.park(this)阻塞当前线程,即w线程
* 如果是超时操作,那么走最后一个逻辑
*/
else if (!timed)
LockSupport.park(this);
/*
* 到这里,表示自旋完毕可以阻塞了(spins小于等于0),并且阻塞之前的检查也做完了(s.waiter 不为 null),没有达到可返回的条件,并且是超时操作
*
* 判断如果剩余超时时间纳秒值大于spinForTimeoutThreshold(1000)
* 那么调用LockSupport.parkNanos(this, nanos)阻塞当前线程(即w线程)nanos纳秒
*
* 否则,由于超时时间过短,此时线程的短时间阻塞和唤醒是不精确的,因此改为一直自旋直到超时或者可以返回为止。
*/
else if (nanos > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanos);
}
}
/**
* 位于TransferStack中的方法
* 判断在s结点的线程阻塞之前是否应该先自旋
*
* @return true 应该自旋 false 不应该
*/
boolean shouldSpin(SNode s) {
//获取此时的栈顶结点h
SNode h = head;
/*
* 如果:
* 1 h == s,为true,即此时的结点s就是栈顶结点,这表示随时都可能新来一个线程与栈顶结点匹配
* 2 或者 h == null ,即此时的栈顶结点为null,这表示双重栈中没有了等待的结点,即刚加入栈中的s结点也被完全匹配并且出栈了。
* 3 或者 isFulfilling(h.mode)方法返回true,表示栈顶结点已被匹配,即有可能s结点也将会随时被匹配了
* 以上三种情况,满足一种,就表示此时可能s已经被匹配了或者有很大概率短时间被匹配,直接返回true,表示需要先自旋一定次数
* 如果此前s已经被匹配了,那么自旋时就会发现并且就不需要阻塞了;如果在自旋过程中被匹配了,那么也不需要阻塞了
*
* 因为这里的阻塞、唤醒操作需要依赖底层系统的mutex互斥量,造成线程在用户状态和内核状态之间的切换,从而浪费时间,
* 在s已被匹配或者很大可能短时间被匹配(s位于栈顶)的情况下使用先自旋一定次数来代替直接阻塞,
* 那么自旋花费的时间肯定远远小于线程阻塞、唤醒需要的时间,有利于提升程序的执行效率。
*/
return (h == s || h == null || isFulfilling(h.mode));
}
/**
* 位于TransferStack中的方法
* 判断栈顶结点是否已匹配
*
* @return true 是 false 否
*/
static boolean isFulfilling(int m) {
//如果栈顶结点已匹配,那么mode = (0或1) | FULFILLING ,结果一定是2 或者 3
//如果 (2或3) & FULFILLING 结果一定是2,即不等于0,因此可以采用位运算来判断是否匹配,并且位运算判断效率更高
return (m & FULFILLING) != 0;
}
2.4.6 clean移除取消结点
clean是位于TransferStack中的方法。用于s结点的awaitFulfill方法返回之后,如果返回值等于s本身,那么表示s被取消了,随后调用clean方法从栈中移除被取消的s结点。
/**
1. 位于TransferStack中的方法
2. 用于awaitFulfill方法返回之后,如果返回值等于s本身,那么表示s被取消了
3. 随后调用clean方法从栈中移除被取消的s结点
4. 5. @param s 被移除的结点s
*/
void clean(SNode s) {
//item和waiter都置为null
s.item = null; // forget item
s.waiter = null; // forget thread
//获取s的后继past
SNode past = s.next;
//如果past不为null 并且 被取消了
if (past != null && past.isCancelled())
//那么past等于past的后继
past = past.next;
// Absorb cancelled nodes at head
/*
* p为最新head,如果p不为null 并且 p不为past 并且 p被取消了
* 那么尝试CAS的将head从 p 指向 p.next,即寻找有效的head,途中移除被取消的结点
* 找到的head:1 为null 2 就是past 3 不为null不是past并且没有取消
*/
SNode p;
while ((p = head) != null && p != past && p.isCancelled())
casHead(p, p.next);
// Unsplice embedded nodes
/*
* 如果p为null,或者p就是post,说明s已经被移除了(可能是自己或者其他结点线程帮忙做的)
* 如果p不为null不是past并且没有取消,那就是第三种情况,此时p(head)可能位于past之前或者之后,那么循环
* 如果位于past之前,从p开始一直循环尝试移除被取消的结点直到,p等于past,后续结点不再遍历
* 如果位于past之后,将会从p开始一直循环尝试移除被取消的结点直到p为null,即栈底部,遍历整个栈
*/
while (p != null && p != past) {
//获取p的后继n
SNode n = p.next;
//如果n不为null并且n被取消了
if (n != null && n.isCancelled())
//尝试CAS的将p.next从n指向n.next,即帮助移除n结点
p.casNext(n, n.next);
/*否则,表示n为null 或者n不为null并且没有被取消*/
else
//否则p设置为n,继续下一次循环
p = n;
}
}
2.4.7 tryMatch尝试匹配
tryMatch是位于SNode中的方法。在s结点或者其他帮助结点的线程请求中,使用m(后一个结点)调用tryMatch方法,参数传入s,尝试匹配新结点s(前一个结点)。
一定是在前一个结点(s)匹配后一个结点成功之后(即CAS将匹配结点压入栈顶成功)才会调用,前一个结点自己会调用,其他结点在帮助结点匹配的时候也可能会调用。即两个结点s和m的互相匹配的操作,实际上是在前一个结点(s)对应的一个线程中或者其他帮助线程中完成的,因为后面的结点对应线程m的线程可能已经阻塞了,自己肯定不能操作,需要在其他结点的线程中帮组匹配并唤醒。
大概步骤为:
- 如果m的match为null,表示没有匹配也没有取消,然后尝试CAS的设置match从null变成s,如果CAS成功:
- 获取waiter阻塞的线程w,如果w不为null(如果m阻塞了,waiter一定不为null),那么waiter重置为null,随后LockSupport.unpark(w)唤醒w线程。
- 返回true。
- 到这一步,说明match可能不为null 或者 match为null但是CAS失败了,返回match是否等于s:如果等于,因为tryMatch可能是其他线程帮忙匹配了,也算匹配成功了,返回true;否则,说明match等于自己,即m结点被取消了,即匹配失败了,返回false。
/**
1. 位于SNode中的方法
2. 在s结点或者其他帮助结点的线程请求中,使用m(后一个结点)调用tryMatch方法,参数传入s,尝试匹配新结点s(前一个结点)
3. <p>
4. 一定是在前一个结点(s)匹配后一个结点成功之后(即CAS将匹配结点压入栈顶成功)才会调用,前一个结点自己会调用,其他结点在帮助结点匹配的时候也可能会调用
5. 即两个结点s和m的互相匹配的操作,实际上是在前一个结点(s)对应的一个线程中或者其他帮组线程中完成的,因为后面的结点对应线程m的线程可能已经阻塞了,需要在其他结点的线程中唤醒
6. 7. @param s 要被匹配的前一个结点
8. @return true 匹配成功 false 匹配失败
*/
boolean tryMatch(SNode s) {
//如果m的match为null,表示没有匹配也没有取消
//然后尝试CAS的设置match从null变成s
if (match == null &&
UNSAFE.compareAndSwapObject(this, matchOffset, null, s)) {
//如果CAS成功,获取waiter阻塞的线程w
Thread w = waiter;
//如果w不为null。如果m阻塞了,waiter一定不为null
if (w != null) { // waiters need at most one unpark
//置为null
waiter = null;
//唤醒w线程
LockSupport.unpark(w);
}
//返回true
return true;
}
/*
* 到这一步,说明CAS失败了,返回match是否等于s:
* 如果等于,因为tryMatch可能是其他线程帮忙匹配了,也算匹配成功了
* 否则,说明match等于自己,即m结点被取消了,即匹配失败了
*/
return match == s;
}
2.4.8 案例分析
非公平模式下,来看看一些操作的过程!
第一次是线程t1的put请求,尝试传递A,它的mode=1。由于head即h为null,那么将会新建一个结点入栈,并且head指向新栈顶:
随后该线程t1,会进入awaitFulfill自旋等待匹配,假设自旋过程中没有被匹配,并且t1阻塞成功,那么此时结构如下:
接着是线程t2,它也是put请求,尝试传递B,它的mode=1。虽然此时h不为null,但是由于h.mode和该请求的mode一致,因此同样将会新建一个结点入栈,并且head指向新栈顶:
随后该t2线程,会进入awaitFulfill自旋等待匹配,假设自旋过程中来了一个新线程t3,他是take请求,尝试获取元素,它的mode=0。由于h不为null并且h.mode和该请求的mode不一致,那么进入第二个else代码块。
判断h是否是匹配结点,明显isFulfilling(h.mode)会返回false,即此时的h不是匹配结点,那么t3会尝试匹配此时的h,即t2线程对应的结点。t3线程会构建一个FULFILLING结点s,next=h,mode=FULFILLING|mode(2|0)=2并入栈,并且head指向新栈顶。CAS成功之后获取s后继m,这个m实际上就是t2线程的结点,获取m的后继mn,这个mn实际上就是t1线程的结点:
随后m调用tryMatch匹配s,尝试将m.match设置为s,由于此时t2还在自旋,因此waiter为null,不必唤醒:
CAS成功之后,这时实际上s和m已经完全匹配成功了,随后t3线程则会尝试调用casHead,将head从s改为mn,即s和m结点出栈,最后返回item,t3的请求结束。
在t2的自旋中,会由于检测到match不再是null,而结束自旋从awaitFulfill方法返回,最后同样返回item,t2的请求结束,最终结构如下:
实际上并没有将m的next置为null,因为如果没有引用指向m和s,那么m和s算作不可达对象,也会被GC回收。如果置为null,可能还会由于其他线程并发访问到null而导致结点信息丢失。
2.5 公平模式原理
公平模式是指底层传输器实例为TransferQueue类型,调用的方法也是TransferQueue内部的方法!
公平模式和非公平模式不同的是,初始化一个TransferQueue对象,将会创建一个Qnode哨兵结点,item为null,isData为false。head和tail都指向该结点。
head永远都指向一个哨兵结点(或者称作已经匹配成功的结点)。需要等待的结点都会被加入到队列尾部,head的后继将会被作为新进来的请求第一个结点匹配的结点,后来尝试匹配的请求不会像TransferStack那样构造一个结点,匹配成功之后原来等待的结点出队列。
这样的话,TransferQueue中等待的结点被匹配的优先级就是入队的先后顺序原则,先入队的先被匹配,这就是公平模式的由来。
2.5.1 transfer匹配结点
TransferStack中的transfer方法用于新请求去匹配一个已存在的请求。作为核心方法,其他内部方法都是在该方法内部被调用的。而开放给用户的外部方法比如put和take也都公用这一个方法,即同时用于传递、获取元素。
大概步骤就是,在一个for循环中尝试下面两种行为:
- 如果队列为空或者队列元素的模式(isData)与当前请求的模式一致,表示当前线程请求模式不能与栈顶结点的模式匹配。那么将会构造一个结点添加到队尾,并向后推进tail引用指向,等待被后来的请求匹配,直到等待超时或者等待时被中断,将会返回null。如果被匹配成功,将会返回传递的数据。最后都会尝试移除构造的结点。
- 如果队列不为空并且尾结点的模式和该请求的模式不一致,表示可以尝试匹配head的后继m。如果m被匹配了后者被取消了,那么尝试帮助m移除队列,随后继续循环与后面的结点匹配,匹配成功之后将会尝试推进head引用指向,并唤醒被匹配结点内部的线程,最后返回传递的数据。如果后面的匹配过程中队列变成了空或者尾结点的模式和该请求的模式一致,那么将会进入第一种情况。
上面的大概步骤比较笼统,而且很多的细节都没有讲到,下面是详细步骤:
- 初始化一个s变量为null,用于构建新结点或者后续重用结点。判断e是否为null,如果为null那么isDate初始化为false,表示消费线程在等待获取数据;否则isDate初始化为true,表示生产线程在等待传输数据。
- 开启一个死循环,尝试匹配结点:
- 获取此时的tail赋给t,即队尾,获取此时的head赋给h,即队头。如果t或者h为null,那么说明双重队列没有初始化,那么结束本次循环,继续下一次循环。
- 如果队列为空,或者尾结点t的模式和该请求的模式一致,这表示当前线程请求模式不能与head.next结点匹配。因为tail是前一个入队的结点,如果当前请求和前一个入队的结点的请求模式一致,说明该请求肯定也不能与head.next结点匹配成功,那么就只有被动的等待被后来的请求匹配了:
- 获取t的后继tn。如果t不为tail,说明尾结点发生了变化,那么结束本次循环,继续下一次循环;
- 如果t为tail,但是后继tn不为null,说明其他线程结点入队,但是还没有来得及改变tail的引用指向,那么调用advanceTail尝试CAS的帮助tail从t指向tn,随后结束本次循环,继续下一次循环。
- 到这里表示t还是此时的tail,并且还没有新结点入队,此时可以构造新结点入队了。如果timed为true 并且 nanos小于等于0,说明是超时操作,并且此时剩余超时时间小于等于0,即超时时间到了,不需要等待匹配了,直接返回null,表示匹配失败,transfer方法结束。
- 如果s为null,那么初始化一个QNode结点赋给s,初始化item为e,isDate 为isDate,这就是将要入队的结点。
- 调用casNext尝试CAS的将t的next从null指向s,即尝试将s加入队列尾部,如果CAS入队失败,表示存在竞争,那么结束本次循环,继续下一次循环。
- CAS将s入队成功之后,继续尝试CAS的将tail从t指向s,即改变队尾属性的指向失败了也没关系,因为在其他线程运行到1.2步骤的时候,可能会帮助该线程推进tail。
- s节点完全入队之后。调用awaitFulfill方法用于结点s的自旋或者阻塞,直到s被匹配,或者s被取消了。如果s被匹配了:如果s是生产请求,那么返回null;如果s是消费请求,返回传递的数据e ;如果s被取消了,返回s自己。返回值使用x来接收。
- 如果x等于s,即s被取消了,那么调用clean尝试从队列中移除s结点,返回null,表示匹配失败,transfer方法结束。
- 到这里,表示s结点被后来的请求成功匹配了。调用isOffList判断s结点是否还未出队(在公平模式下,如果一个结点被成功匹配,那么该结点一定是head的后继)。如果是:
- 那么此时可能连head也没有设置,那么尝试CAS的将head从t指向s,推进head的指向,将s变成head,失败也没关系,可能是匹配的线程或者其他已经做了这个操作。
- 如果x不为null,表示s结点代表一个消费者请求,此时item指向传递的数据,那么s的item指向自己,释放传递的数据的引用。
- s的waiter清空,释放线程引用。
- 如果x不为null,表示s结点代表一个消费者请求,x就是传递的数据,那么返回x;如果x为null,表示s结点代表一个生产者请求,e就是传递的数据,那么返回e。即两个成功匹配的请求都会返回它们传递的数据,transfer方法结束。
- 如果队列不为空并且尾结点的模式和该请求的模式不一致,这表示当前线程的请求模式可以主动尝试与head.next结点的匹配:
- 获取h的后继m,这才是需要匹配的结点,因为双重队列的head永远指向一个哨兵结点。如果此时队列结构被改变了,那么结束本次循环,继续下一次循环。
- 获取m的item值x。判断x是否不为null,如果判断的结果和本次请求的isData一致,说明m结点被匹配了,第一个表达式为true;否则,判断x是否等于m,如果是,那么表示m结点被取消了,第二个表达式为true;否则,可以尝试匹配m,由m调用casItem,尝试将m的item值CAS的从x设置为e,这就是m匹配s的过程(和非公平模式一样,由原来的结点匹配新来的请求,从这里可以看出来:如果当前请求是一个生产请求,那么它匹配的m的item将会被设置为要传递的数据e;如果当前请求是一个消费请求,那么它匹配的m的item将会被设置为null),如果CAS失败,表示匹配失败,第三个表达式为true:
- 如果上面三个表达式任意一个为true,那么说明:m结点已经被匹配了,或者m结点被取消了,或者CAS操作的同时m结点被取消了,或者被其他请求抢先匹配了。那么调用advanceHead尝试CAS的将head从h指向nh,帮助移除原来的head,然后结束本次循环,继续下一次循环。
- 到这里,说明m匹配s成功了,同样尝试CAS的将head从h指向m,将原来的出队,失败也没关系,可能是匹配的线程或者其他线程已经帮助做了这个操作。
- unpark尝试唤醒m内部阻塞的线程。这里x是最开始m的item,如果x不为null,表示m结点代表一个生产者请求,x就是传递的数据,那么返回x;如果x为null,表示m结点代表一个消费者请求,e就是传递的数据,那么返回e。即两个成功匹配的请求都会返回它们传递的数据,transfer方法结束。
经过上面的步骤,我们发现如果一个请求和一个结点相互匹配成功,那肯定是put和take请求相互匹配,并且那么最终两个transfer方法都会返回传输的数据。
一次比较完整的匹配过程如下:
- 新来的一个请求,如果发现此时队列不为空并且队列尾结点的模式和该请求的模式不一致,这表示当前线程请求模式可以尝试与head.next结点的匹配,实际上它总是与head的后继结点m结点匹配。但是还没完,这相当于前驱请求确认了,它还必须要等m结点确认,这样这样才算一个新来的请求和一个等待的结点真正的匹配成功!我们发现,新来的请求确定可匹配结点的过程并没有构造新结点入队,这是和非公平模式的一个区别。
- 然后就是首先校验m是否已经被其他请求匹配了,以及m是否已经取消了,如果是这两种情况之一,那么调用advanceHead尝试将head指向m,然后重新循环,假设此时队列不为空并且队列尾结点的模式和该请求的模式不一致还是成立,那么那么将会由上一轮的m的后继—这一轮的head的后继与该请求匹配;
- 匹配的过程实际上就是一个CAS操作,首先保存m的item值x,然后在当前请求的线程中,m调用casItem尝试将m结点的item设置为当前请求的e参数,即如果当前请求是一个生产请求,那么它匹配的m一定是消费请求,m的item将会被设从null置为要传递的数据e;如果当前请求是一个消费请求,那么它匹配的m一定是生产请求,m的item将会被从传递的数据设置为null。
- 假设CAS匹配成功之后,同样会调用调用advanceHead尝试将head指向m,即尝试把已被匹配的结点出队,利于后面的请求继续匹配。随后唤醒m结点内部在等待的线程。这里x是第3步最开始获取的m的item。如果x不为null,表示m结点代表一个生产者请求,x就是传递的数据,那么返回x;如果x为null,表示m结点代表一个消费者请求,e就是接收的数据,那么返回e,即两个成功匹配的请求都会返回它们传递的数据,方法结束。一次数据传递执行完毕。
- 还有可能就是m被匹配、被取消、在CAS匹配失败之后,如果在下一次循环时发现队列空了,那么将会走if代码块的逻辑,新的请求可能会构建一个等待结点加入队列尾部,此时该结点以及请求只有被动的等待被另外的新来的请求匹配成功,或者等待中断、或者等待超时的情况才能返回了。
/**
* 位于TransferQueue中的方法
* 传递、获取一个元素
*
* @param e 如果非空,则表示生产者要交给消费者的数据; 如果为null,则表示消费者请求获取生产者的数据
* @param timed 如果是超时操作,那么为true
* @param nanos 超时时间,以纳秒为单位
* @return 如果返回的结果不为null,那么表示数据被转移了或获取了;如果为null,这个操作可能因为超时或者被中断而失败了。
*/
@SuppressWarnings("unchecked")
E transfer(E e, boolean timed, long nanos) {
//初始化一个s变量为null,用于构建新结点或者重用结点
QNode s = null; // constructed/reused as needed
/*
* 判断e是否为null
* 如果为null那么isDate初始化为false,表示消费线程在等待获取数据
* 否则isDate初始化为true,表示生产线程在等待传输数据
*/
boolean isData = (e != null);
/*开启一个死循环,尝试匹配结点*/
for (; ; ) {
//获取此时的tail赋给t,即队尾
QNode t = tail;
//获取此时的head赋给h,即队头
QNode h = head;
/*
* 如果t或者h为null,那么说明双重队列没有初始化,那么结束本次循环,继续下一次循环,通常是不会出现这样的情况的
* 初始化的双重队列时,head和tail都将会指向一个哨兵结点,该哨兵结点的item为null 并且 idDate为false
*/
if (t == null || h == null) // saw uninitialized value
continue; // spin
/*
* 1 如果队列为空,或者尾结点的模式和该请求的模式一致,这表示当前线程请求模式不能与head.next结点匹配
* 因为tail是前一个入队的结点,如果当前请求和前一个入队的结点的请求模式一致,
* 说明该请求肯定也不能与head.next结点匹配成功,那么就只有被动的等待被后来的请求匹配了
*/
if (h == t || t.isData == isData) { // empty or same-mode
//获取t的后继tn
QNode tn = t.next;
/*
* 1.1 如果t不为tail,说明尾结点发生了变化,那么结束本次循环,继续下一次循环
* */
if (t != tail) // inconsistent read
continue;
/*
* 1.2 如果t为tail,但是后继tn不为null,说明其他线程结点入队,但是还没有来得及改变tail的引用指向
*/
if (tn != null) { // lagging tail
//向前推进tail,就是尝试CAS的帮助tail从t指向tn,随后结束本次循环,继续下一次循环
//失败了也没关系,这说明其他线程帮助成功了
advanceTail(t, tn);
continue;
}
//到这里表示t还是此时的tail,并且还没有新结点入队,此时可以构造新结点入队了
/*
* 1.3 如果timed为true 并且 nanos小于等于0
* 说明是超时操作,并且此时剩余超时时间小于等于0,即超时时间到了,不需要等待匹配了
*/
if (timed && nanos <= 0) // can't wait
//直接返回null
return null;
//如果s为null
if (s == null)
//那么初始化一个QNode结点赋给s,这是将要入队的结点
//初始化item为e,isDate 为 isDate
s = new QNode(e, isData);
//完整的入队成功分为两步,第一步是CAS的将s结点加入到队列尾部,第二步是CAS的将tail指向s结点
/*
* 1.4 尝试CAS的将t的next从null指向s,即尝试将s入队
*/
if (!t.casNext(null, s)) // failed to link in
//如果CAS入队失败,表示存在竞争,那么结束本次循环,继续下一次循环
continue;
/*
* 1.5 CAS将s入队成功之后,继续尝试CAS的将tail从t指向s,即改变队尾属性的指向
* 失败了也没关系,因为在其他线程运行到1.2步骤的时候,可能会帮助该线程推进tail
*/
advanceTail(t, s); // swing tail and wait
/*
* 1.6 调用awaitFulfill方法用于结点s的自旋或者阻塞,直到s被匹配,或者s被取消了
*
* 如果s被匹配了:如果s是生产请求,那么返回null;如果s是消费请求,返回传递的数据e ;如果s被取消了,返回s自己
*/
Object x = awaitFulfill(s, e, timed, nanos);
//如果x==s 为true,即s被取消了
if (x == s) { // wait was cancelled
//调用clean尝试从队列中移除s结点
clean(t, s);
//返回null
return null;
}
/*
* 到这里,表示s结点被后来的请求成功匹配了。
* 调用isOffList判断s结点是否还未出队
*/
//如果s还未出队,即在被匹配成功到执行到这里之间还没有进行其他的匹配操作
if (!s.isOffList()) { // not already unlinked
//在公平模式下,如果一个结点被成功匹配,那么该结点一定是head的后继
//那么尝试CAS的将head从t指向s,推进head的指向,将s变成head
//失败也没关系,可能是匹配的线程或者其他已经做了这个操作
advanceHead(t, s); // unlink if head
//如果x不为null,表示s结点代表一个消费者请求,此时item指向传递的数据
if (x != null) // and forget fields
//s的item指向自己,释放传递的数据的引用
s.item = s;
//s的waiter清空,释放线程引用
s.waiter = null;
}
//如果x不为null,表示s结点代表一个消费者请求,x就是接收的数据,那么返回x
//如果x为null,表示s结点代表一个生产者请求,e就是传递的数据,那么返回e
//即两个成功匹配的请求都会返回它们传递的数据
return (x != null) ? (E) x : e;
}
/*
* 2 如果队列不为空并且尾结点的模式和该请求的模式不一致,这表示当前线程请求模式可以尝试与head.next结点的匹配
*/
else { // complementary-mode
//获取h的后继m,这才是需要匹配的结点,双重队列的head永远指向一个哨兵结点
QNode m = h.next; // node to fulfill
//如果此时队列结构被改变了,那么结束本次循环,继续下一次循环
if (t != tail || m == null || h != head)
continue; // inconsistent read
//获取后继m的item值x
Object x = m.item;
/*
* 1 isData == (x != null)
* 判断x是否不为null,如果判断的结果和本次请求的isData一致,说明m结点被匹配了
* 比如当前请求是一个生产请求,那么isData为true,m的isData肯定是false,表示消费请求
* 但是如果m的item值x不等于null,那么说明m已经和另一个生产请求匹配了,
* 下面的m.casItem(x, e),会设置等待的消费请求的item为要传递的数据e,返过来将会设置等待的生产请求的item为null
* 2 x == m
* 判断x是否等于m,如果是,那么表示m结点被取消了
*
* 3 !m.casItem(x, e))
* 尝试将m的item值CAS的从x设置为e,这就是m匹配s的过程;
* 如果CAS成功,表示匹配成功;
* 如果CAS失败,表示匹配失败,可能是被取消了,或者被其他请求抢先匹配了
*
*
* 从这里可以看出来:
* 如果当前请求是一个生产请求,那么它匹配的m一定是消费请求,m的item将会被从null设置为要传递的数据e;
* 如果当前请求是一个消费请求,那么它匹配的m一定是生产请求,m的item将会被从传递的数据设置为null。
*/
if (isData == (x != null) || // m already fulfilled
x == m || // m cancelled
!m.casItem(x, e)) { // lost CAS
/*
* 到这里,说明:
* m结点已经被匹配了
* 或者 m结点被取消了
* 或者 m结点匹配s的CAS过程失败,这表示CAS操作的同时m结点被取消了,或者被其他请求抢先匹配了
* 以上三种情况满足一种,尝试CAS的将head从h指向nh,帮助移除原来的head,然后结束本次循环,继续下一次循环
*/
advanceHead(h, m); // dequeue and retry
continue;
}
//到这里,说明m匹配s成功了,同样尝试CAS的将head从h指向m,移除原来的head
//失败也没关系,可能是匹配的线程或者其他线程已经帮助做了这个操作
advanceHead(h, m); // successfully fulfilled
//唤醒m内部保存的线程
LockSupport.unpark(m.waiter);
//这里x是最开始m的item
//如果x不为null,表示m结点代表一个生产者请求,x就是传递的数据,那么返回x
//如果x为null,表示m结点代表一个消费者请求,e就是接收的数据,那么返回e
//即两个成功匹配的请求都会返回它们传递的数据
return (x != null) ? (E) x : e;
}
}
}
2.5.2 isCancelled结点是否取消
isCancelled方法是位于QNode结点类中的方法,由某个QNode结点调用,用于判断调用结点是否被取消了。
想要判断是否被取消很简单,就是看该结点的item属性是否指向自己,什么时候才会指向自己呢?实际上在awaitFulfill方法中,即等待被匹配的时候,如果等待过程中(自旋或者阻塞)线程被中断了或者由于是超时请求并且超时时间到了也没有匹配成功。这两种情况下都会调用tryCancel方法将等待结点的item属性指向自己,表示等待被取消。
被取消的结点将会被移除栈,并且结点对应的请求的transfer方法将会返回null。另外,等待的消费请求结点s被成功匹配之后,最后也会将s.item属性指向s自己,但这不是被取消的意思,而已清除多余的引用关系。
/**
* 位于QNode中的方法
* 通过判断item是否等于自身来判断当前结点是否被取消
*
* @return true 是 false 否
*/
boolean isCancelled() {
return item == this;
}
2.5.1 isOffList结点是否出队
isOffList方法是位于QNode结点类中的方法,由某个QNode结点调用,用于判断调用结点是否已经出队。
如果调用结点已经出队,那么结点的next属性将会指向自己,该方法只有在等待的结点s被匹配成功之后,在s结点对应的线程中会调用。
实际上如果一个结点s被匹配了,那么仅仅会将head的引用指向为s,即将s作为哨兵结点,而真正出队的是原来的head,再一次匹配过程中s是不会出队的,这里判断是否出队的意思就是,是否有新的请求成功的完成了后面的匹配,如果新的请求完成了后续的匹配,那么s肯定作为head结点被出队了。那么s结点会被回收,什么也不做。
而如果还没有出队,那么s结点还在队列中,甚至有可能连head也没有设置,因此尝试将s设置为head,如果s是消费请求,s的item指向自己,释放传递的数据的引用,还会将waiter置空,释放线程的引用。
/**
1. 位于QNode中的方法
2. 判断调用结点是否已经出队
3. 如果调用结点已经出队,那么next将会指向自己
4. 5. @return true 已出队 false 未出队
*/
boolean isOffList() {
return next == this;
}
2.5.2 CAS操作
TransferQueue中对于引用属性的赋值基本都是使用的CAS操作,这样能够保证单个变量符合操作的原子性,同时避免了锁的使用,提升了性能。
常用的有如下CAS方法:
- advanceHead,位于TransferQueue中的方法,尝试CAS的改变队头head属性的指向,从h指向nh;
- advanceTail,位于TransferQueue中的方法,尝试CAS的改变队尾tail属性的指向,从t指向nt;
- casNext,位于QNode中的方法,尝试CAS的改变调用结点next属性的指向,从cmp指向val;
- tryCancel,位于QNode中的方法,尝试CAS的改变调用结点item属性的指向,从cmp指向自己,实际上这就是结点取消的方法;
- casItem,位于QNode中的方法,尝试CAS的改变调用结点item属性的指向,从cmp指向val,实际上这就是m结点匹配新请求的方法;
/**
* 位于TransferQueue中的方法
* 尝试CAS的将head从h指向nh
*
* @param h 预期head
* @param nh 新head
*/
void advanceHead(QNode h, QNode nh) {
//如果h此时还是等于head 并且 将head从h指向nh成功,那么返回true;否则返回false
if (h == head &&
UNSAFE.compareAndSwapObject(this, headOffset, h, nh))
//CAS成功,还会将h的next指向自己,用于移除原head结点
h.next = h; // forget old next
}
/**
* 位于TransferQueue中的方法
* 尝试CAS的将tail从t指向nt
*
* @param t 预期tail
* @param nt 新tail
*/
void advanceTail(QNode t, QNode nt) {
//如果tail此时还是等于t 并且 将tail从t指向nt成功,那么返回true;否则返回false
if (tail == t)
UNSAFE.compareAndSwapObject(this, tailOffset, t, nt);
}
/**
* 位于QNode中的方法
* 尝试CAS的将调用结点的next从cmp指向val
*
* @param cmp 预期next
* @param val 新next
* @return true 成功 false 失败
*/
boolean casNext(QNode cmp, QNode val) {
//如果next此时还是等于cmp 并且 将next从cmp指向val成功,那么返回true;否则返回false
return next == cmp &&
UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
/**
* 位于QNode中的方法
* 即尝试CAS的将item的值由cmp指向自己结点
* 尝试取消等待结点的方法
*
* @param cmp 预期item
*/
void tryCancel(Object cmp) {
UNSAFE.compareAndSwapObject(this, itemOffset, cmp, this);
}
/**
1. 位于QNode中的方法
2. 尝试CAS的将调用结点的item从cmp指向val
3. 这就是m结点尝试匹配新请求的方法
4. 5. @param cmp 预期item
5. @param val 新item
6. @return true 成功 false 失败
*/
boolean casItem(Object cmp, Object val) {
//如果item此时还是等于cmp 并且 将item从cmp指向val成功,那么返回true;否则返回false
return item == cmp &&
UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
}
2.5.3 awaitFulfill等待匹配
awaitFulfill是位于TransferQueue中的方法,如果队列为空,或者尾结点的模式和该请求的模式一致,即当前线程请求模式不能与head.next结点的匹配。判断如果该请求不是超时操作或者是超时操作但是剩余时间还大于0,那么会尝试构建新结点s加入队列尾部,并尝试更新tail指向s结点,如果上面的判断都满足、操作都成功,随后就会调用awaitFulfill方法,用于结点s的自旋或者阻塞,直到s被匹配,或者s被取消了才会返回。
这个方法也很重要且难以理解,但是和TransferStack中的原理差不多,大概步骤为:
- 判断是否是超时操作,如果是,那么计算超时截止的时间点的纳秒值;否则为0,使用deadline保存。获取当前结点s的线程w。
- spins表示自旋次数,这里计算自旋次数。调用head.next == s判断在s结点的线程阻塞之前是否需要先自旋。如果需要,那么继续判断是否是超时操作,如果是,那么spins=maxTimedSpins;否则spins=maxUntimedSpins。如果不需要,那么spins=0,即自旋次数为0。
- 开启一个死循环,直到被匹配或者取消等待才能返回:
- 首先,判断的是w线程是否被中断了。如果被中断了,那么尝试调用tryCancel方法CAS的直接取消s结点,即s.item指向s自己,失败也没关系,这里对于item属性的CAS竞争主要是在casItem方法和tryCancel方法之间,无论它们谁CAS成功,item都将不会等于s结点原本的item值e,因此在下面的判断中将会返回。
- 再次获取s的item值x,判断此时的x和s结点原本的item值e是否不相等。如果是,直接返回x,awaitFulfill方法结束。
- 判断是否是超时操作,如果是,那么计算剩余超时时间纳秒nanos,如果nanos小于等于0,表示超时时间到了,但是还没有被匹配,那么同样尝试调用tryCancel方法CAS的直接取消s结点,即s.item指向s自己,随后continue结束本次循环,继续下一次循环,在下一次循环中将会因为x不等于e而返回。
- 到这里,说明s没有被中断,并且s也没有被匹配,并且不是超时操作或者是超时操作但是时间还没用完。但这都是瞬时的状态,随时都可能改变。
- 首先判断spins即自旋次数是否大于0。如果大于0,那么spins自减1,即自旋次数减少一次,然后结束本次循环,继续下一次循环。
- 否则,表示spins<=0,即自旋完毕还没有返回,表示可以阻塞了。判断如果s.waiter等于null,那么s.waiter = w,然后结束本次循环,继续下一次循环。
- 否则,表示自旋完毕可以阻塞了(spins小于等于0),并且阻塞之前的检查也做完了(s.waiter 不为 null),没有达到可返回的条件。判断如果不是超时操作,那么调用LockSupport.park(this)阻塞当前线程,即w线程,等待被唤醒。
- 否则,表示是超时操作,判断如果nanos大于spinForTimeoutThreshold(1000),那么调用LockSupport.parkNanos(this, nanos)阻塞当前线程(即w线程)nanos纳秒,等待被唤醒或者超时时间到。否则,继续下一次循环,即一直自旋直到超时或者可以返回为止。
这里有一些细节:
- 实际上在等待过程中,通常都是先自旋一定次数等待,如果此期间被匹配了或者取消了,那么结点对应的线程也就不必阻塞了。否则,自旋完毕还没有被匹配或者取消,那么对象对应的线程将会阻塞。为什么要先自旋呢?因为这里的阻塞、唤醒操作需要依赖底层系统的mutex互斥量,造成线程在用户状态和内核状态之间的切换,从而浪费时间,s是head的后继的情况下,大概率有新的请求进来并且和该结点能够进行匹配。因此使用先自旋一定次数来代替直接阻塞,如果在此期间被匹配了,那么自旋花费的时间肯定远远小于线程阻塞、唤醒需要的时间,有利于提升程序的执行效率。
- 在第一个if判断中,即使spins自旋次数初始化为0或者因为自旋完毕变成了0。s也会进入for循环走一遍流程,检查是否达到了返回的条件。因为在刚进入awaitFulfill方法的时候,就有可能被匹配了、中断了、超时了,此时不需要阻塞。即,spins=n,那么上面的判断将会走n+1次。
- 在第二个elsf if判断中,在第一次spins小于等于0的时候,表示自旋完毕也没有返回,可以阻塞了。但是这里并没有直接阻塞,还会判断如果s.waiter等于null,那么s.waiter = w,然后结束本次循环,继续下一次循环。从这里我们能看出,即使自旋完毕可以阻塞了,在真正的阻塞之前,还会至少再循环一次检查是否达到了返回的条件,因为状态是随时都可能会改变的:
- 实际上,如果这里将waiter置为w,此时如果有一个新线程进来,在transfer方法的else代码块中刚好正在与该结点匹配,那么会调用CASItem方法改变s.item的值,这样在下一次循环中,由于x不等于e,s就可以退出循环了,而不必阻塞;
- 另外即使w线程最终阻塞了,后来的线程在transfer方法的else代码块中设置了s.item的值之后,会直接LockSupport.unpark(w)唤醒w线程,这样w在被唤醒之后的下一个循环中将会由于满足条件而退出循环。
- 而在最坏的情况下:unpark方法先于park方法调用,此时由于LockSupport内部“许可”的原理,此前unpark给予一个许可,后续park方法也将会直接返回而不会阻塞,在下一个循环中同样将会由于满足条件而退出循环!
- 在超时操作的调用中,如果剩余超时时间纳秒值小于等于1000,那么改为一直自旋直到超时或者可以返回为止。因为Java中超时阻塞线程的时候方法会在线程状态切换以及底层mutex资源申请、释放时耗费一定时间,如果超时时间较短,那么短时间的超时等待是不精确的,还不如一直自旋更加精确。
/**
1. 位于TransferQueue中的方法
2. 用于结点的自旋或者阻塞,直到被匹配或者取消等待才会返回
3. 4. @param s 自旋或者阻塞的结点
5. @param e s结点原本的item值,如果非空,则表示生产者要交给消费者的数据; 如果为null,则表示消费者请求获取生产者的数据
6. @param timed 如果是超时操作,那么为true
7. @param nanos 超时时间,纳秒
8. @return 如果s被匹配了:如果s是生产请求,那么返回null;如果s是消费请求,返回传递的数据e ;如果s被取消了,返回s自己
*/
Object awaitFulfill(QNode s, E e, boolean timed, long nanos) {
/* Same idea as TransferStack.awaitFulfill */
//判断是否是超时操作,如果是,那么计算超时截止的时间点的纳秒值;否则为0,使用deadline保存
final long deadline = timed ? System.nanoTime() + nanos : 0L;
//获取当前结点s的线程w
Thread w = Thread.currentThread();
/*
* spins表示自旋次数,这里计算自旋次数
* 首先调用 head.next == s 判断在s结点的线程阻塞之前是否需要先自旋,即如果s作为head的后继,那么表示随时都可能被匹配,那么可以自旋
* 如果需要,那么继续判断是否是超时操作:
* 如果是,那么spins=maxTimedSpins,即超时等待的线程在阻塞之前应该自旋的次数,如果CPU可用线程数量小于2,那么自旋没有太大意义,因此自旋次数为0
* 否则spins=maxUntimedSpins,即非超时等待的线程在阻塞之前应该自旋的次数,如果CPU可用线程数量小于2,那么自旋没有太大意义,因此自旋次数为0
* 如果不需要,那么spins=0,即自旋次数为0
*/
int spins = ((head.next == s) ?
(timed ? maxTimedSpins : maxUntimedSpins) : 0);
/*死循环,直到被匹配或者取消才能返回*/
for (; ; ) {
//首先是一系列是否满足返回的条件的判断
/*首先 判断的是w线程是否被中断了*/
if (w.isInterrupted())
/*
* 如果被中断了,那么尝试CAS的直接取消s结点,即s.item指向s自己
*/
s.tryCancel(e);
//再次获取s的item值x
Object x = s.item;
/*
* 这里判断满足返回的条件是通过item字段来判断的,而item字段又被用来表示生产请求或者消费请求,因此需要将他们区分开
*
*
* 如果s被匹配了:
* 如果s是生产请求,那么s.item从传递的数据e指向null;
* 如果s是消费请求,那么s.item从null指向传递的数据e,
* 上面这两种改变都使得新item和它们原本的item值e不一致,因此可以返回
* 如果s被取消了:
* s.item 从传递的数据e或者null 指向s自己
*
* 如果x不等于e,说明s被匹配成功了,或者被取消了,都可以返回
*/
if (x != e)
//直接返回x,不需要自旋和阻塞,这是这个死循环唯一的出口
return x;
/*然后 判断是否是超时操作*/
//如果是超时操作
if (timed) {
//计算剩余超时时间纳秒nanos
nanos = deadline - System.nanoTime();
//如果nanos小于等于0,表示超时时间到了,但是还没有被匹配
if (nanos <= 0L) {
/*那么尝试直接取消s结点,s.item指向s自己*/
s.tryCancel(e);
//结束本次循环,继续下一次循环,在下一次循环中将会因为x != e不为null而返回
continue;
}
}
//然后是一系列是否继续自旋还是阻塞的判断
/*
* 到这里,说明:
* s没有被中断,并且s没有被匹配,并且不是超时操作或者是超时操作但是时间还没用完
*
* 首先判断spins即自旋次数是否大于0
*/
if (spins > 0)
/*
* 如果大于0,那么spins自减1,即自旋次数减少一次,然后结束本次循环,继续下一次循环
* 从这里我们能看出,即使spins自旋次数初始化为0或者因为自旋完毕变成了0
* s也会进入for循环走一遍流程,检查是否达到了返回的条件。
* 因为在刚进入awaitFulfill方法的时候,就有可能被唤醒了或者中断了,此时检查一遍就可以不需要阻塞。
* 即,spins=n,那么上面的判断将会走n+1次
*/
--spins;
/*
* 到这里,表示spins<=0,即自旋完毕还没有返回,表示可以阻塞了
* 判断如果s.waiter等于null,那么s.waiter = w,然后结束本次循环,继续下一次循环
* 从这里我们能看出,即使自旋完毕可以阻塞了,在真正的阻塞之前,还会至少再循环一次检查是否达到了返回的条件。
*
* 实际上,如果这里将waiter置为w,此时如果有一个新线程进来,在transfer方法的else代码块中刚好正在与该结点匹配,
* 那么会调用CASItem方法改变s.item的值,这样在下一次循环中,由于x不等于e,s就可以退出循环了,而不必阻塞
*
* 另外即使w线程最终阻塞了,后来的线程在transfer方法的else代码块中设置了s.item的值之后,
* 会直接LockSupport.unpark(w)唤醒w线程,这样w在被唤醒之后的下一个循环中将会由于满足条件而退出循环
*
* 而在最坏的情况下:unpark先于park调用,此时由于LockSupport内部“许可”的原理,
* 此前unpark给予一个许可,park方法也将会直接返回而不会阻塞,在下一个循环中同样将会由于满足条件而退出循环
*/
else if (s.waiter == null)
s.waiter = w;
/*
* 到这里,表示自旋完毕可以阻塞了(spins小于等于0),并且阻塞之前的检查也做完了(s.waiter 不为 null),没有达到可返回的条件
* 判断如果不是超时操作,那么调用LockSupport.park(this)阻塞当前线程,即w线程
* 如果是超时操作,那么走最后一个逻辑
*/
else if (!timed)
LockSupport.park(this);
/*
* 到这里,表示自旋完毕可以阻塞了(spins小于等于0),并且阻塞之前的检查也做完了(s.waiter 不为 null),没有达到可返回的条件,并且是超时操作
*
* 判断如果剩余超时时间纳秒值大于spinForTimeoutThreshold(1000)
* 那么调用LockSupport.parkNanos(this, nanos)阻塞当前线程(即w线程)nanos纳秒
*
* 否则,由于超时时间过短,此时线程的短时间阻塞和唤醒是不精确的,因此改为一直自旋直到超时或者可以返回为止。
*/
else if (nanos > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanos);
}
}
2.5.4 clean移除取消结点
clean是位于TransferStack中的方法。用于awaitFulfill方法返回之后,如果返回值等于s本身,那么表示s被取消了,随后调用clean方法从队列中移除被取消的s结点。这里的删除没那么简单,需要分情况:
- 如果s不是尾部结点,那么直接casNext将s移除队列即可;
- 如果s是尾部结点,那么永远不会在本次clean方法中移除位于队列尾部的结点。首先会将上一次cleanMe记录的已被取消的尾部结点清理(如果cleanMe不为null),然后本次的尾部结点的前驱使用cleanMe保存,等待下一次出现取消的结点是尾节点的时候再清理,即延迟删除。(但是在这段时间中也可能被其他take、put线程清理了)。
即使所有的结点都被取消了,最终head和tail还是会指向位于队尾的被取消的哨兵结点,即永远都不会指向null。
如果clean中的逻辑仅仅是casNext,即s的前驱prev.next指向s的后继sn,如果s为尾结点,那么sn可能在此时为null,与此同时,另一个新请求进来可能由于没有匹配成功而构造结点加入队列尾部成为s的后继,于是造成了结点的丢失。
因此,队尾的结点不会被在本次删除中被直接删除,而是cleanMe记录下来(记录它的前驱)。下一次clean方法被调用并且被清除的结点作为队尾结点时,说明上一次的s结点肯定不是队尾结点了,因此可以清除,随后下一次的被清除结点同样使用cleanMe保存。
/**
* 位于TransferQueue中的方法
* 尝试将s结点从队列中移除
*
* @param pred s的前驱
* @param s 需要被移除的结点
*/
void clean(QNode pred, QNode s) {
//waiter置为null
s.waiter = null; // forget thread
//死循环,如果s的前驱的后继为s
while (pred.next == s) { // Return early if already unlinked
//获取此时的head
QNode h = head;
//获取head的后继结点
QNode hn = h.next; // Absorb cancelled first node as head
//如果后继不为null 并且 后继被取消了
if (hn != null && hn.isCancelled()) {
//尝试CAS的将head从h指向hn,即指向原head的后继
advanceHead(h, hn);
//结束本次循环,继续下一次循环
continue;
}
//到这里,表示hn为null,或者hn不为null并且没有被取消
//获取此时的tail
QNode t = tail; // Ensure consistent read for tail
//如果t == h,表示队列为空,保留一个哨兵结点,直接返回,clean方法结束
if (t == h)
return;
//获取t的后继
QNode tn = t.next;
//如果t不为tail,说明尾结点发生了变化,那么结束本次循环,继续下一次循环
if (t != tail)
continue;
//如果t为tail,但是后继tn不为null,说明其他线程结点入队,但是还没有来得及改变tail的引用指向
if (tn != null) { // lagging tail
//向前推进tail,就是尝试CAS的帮助tail从t指向tn,随后结束本次循环,继续下一次循环
//失败了也没关系,这说明其他线程帮助成功了
advanceTail(t, tn);
continue;
}
//到这里表示t还是此时的tail,并且还没有新结点入队
//如果s不等于t,表示s不是尾节点,可以直接清除
if (s != t) { // If not tail, try to unsplice
//获取s的后继
QNode sn = s.next;
/*
* 如果后继sn等于s,表示已经被出advanceHead方法队列了。
* 或者后继不等于s,但是CAS将前驱的后继设置为sn成功
* 以上两种情况满足一种,表示s已被清除,直接返回,clean方法结束
*/
if (sn == s || pred.casNext(s, sn))
return;
}
/*
* 到这里,可能是:
* s是尾结点
* CAS清除失败,可能是前驱pred作为head正在出队,此时pred的next指向自身
*
* 下面的操作将会删除上一次未删除的s结点,同时不会删除这一次的s结点
*/
//dp保存此时的cleanMe结点
QNode dp = cleanMe;
/*如果dp不为null,那么尝试删除上一次的s结点*/
if (dp != null) { // Try unlinking previous cancelled node
//获取dp的后继d,就是上一次的s结点,dp就是上一次的pred结点
QNode d = dp.next;
QNode dn;
/*
* d == null ,表示dp,即上一次的pred被clean方法移除队列了
* 或者 d == dp,表示dp,即上一次的pred作为head被advanceHead方法移除队列了
* 或者 !d.isCancelled(),表示d还没有被取消
* 或者 d不为tail并且 d的后继dn不为null 并且 dn不等于d ,表示原来的s结点后面存在结点,那么可以尝试清除上一次的s
* 最后一步dp.casNext(d, dn),就是清理上一次的被删除的s结点的过程,这个清理操作相当于从cleanMe中过来的
*
* 以上情况满足一种,那么表示上一次的s结点被清理了,因此可以清理cleanMe引用
*/
if (d == null || // d is gone or
d == dp || // d is off list or
!d.isCancelled() || // d not cancelled or
(d != t && // d not tail and
(dn = d.next) != null && // has successor
dn != d && // that is on list
dp.casNext(d, dn))) // d unspliced
//清理cleanMe引用,尝试CAS的将cleanMe从dp指向null
casCleanMe(dp, null);
/*
* 如果dp等于前驱pred,那么后继就是s,相当于完成了下面else if 的操作
* 等到下一次再清除这一次的s结点,直接返回
*/
if (dp == pred)
return; // s is already saved node
}
/*
* 如果dp为null,那么尝试CAS的将cleanMe从null指向pred,即保存这一次的s
* 等到下一次再清除这一次的s结点,直接返回
*/
else if (casCleanMe(null, pred))
//等到下一次再清除
return; // Postpone cleaning s
}
}
/**
* 位于TransferQueue中的方法
* 尝试CAS的将cleanMe从dp指向null
*
* @param cmp 预期cleanMe
* @param val 新cleanMe
* @return true 成功 false 失败
*/
boolean casCleanMe(QNode cmp, QNode val) {
//如果cleanMe此时还是等于cmp 并且 将cleanMe从cmp指向val成功,那么返回true;否则返回false
return cleanMe == cmp &&
UNSAFE.compareAndSwapObject(this, cleanMeOffset, cmp, val);
}
2.5.1 案例分析
公平模式下,来看看一些操作的过程!
初始化TransferQueue的时候就会构造一个哨兵结点,head和tail都指向该结点:
第一次是线程t1的put请求,尝试传递A,它的isData=true。由于h等于t,即队列为空,那么将会新建一个结点加入队尾,并且tail指向新队尾:
随后该线程t1,会进入awaitFulfill自旋等待匹配,假设自旋过程中没有被匹配,并且t1阻塞成功,那么此时结构如下:
接着是线程t2,它也是put请求,尝试传递B,它的isData=true。虽然此时h不等于t,但是由于t.isData和该请求的isData一致,因此同样将会新建一个结点加入队尾,并且tail指向新队尾:
随后该t2线程,会进入awaitFulfill自旋等待匹配,假设自旋过程中来了一个新线程t3,他是take请求,尝试获取元素,它的isData=false。由于h不为null并且h.mode并且t.isData该请求的isData不一致,那么进入第二个else代码块。
获取h的后继m,这里的m就是t1线程对应的结点,判断m是否是匹配结点或者已被取消,这里m都不是,那么t3会尝试匹配此时的m,即t1线程对应的结点。
这里的匹配很简单,t3不需要构建新结点,而是先保存m的item值x,随后m调用casItem将item的值改成t3的e参数,即null,CAS成功即表示匹配成功:
匹配成功之后,head被设置为m结点,由于此时t1线程被阻塞了,因此unpark唤醒t1,最后返回传递的数据,t3的请求结束。
t1在被唤醒之后,发现item值不一致了,因此也会从awaitFulfill方法返回,返回值为null,不等于结点自己,因此表示成功匹配,随后清理引用,最后返回传递的数据,t1的请求结束,最终结构如下:
2.6 入队操作
2.6.1 put(e)方法
public void put(E o)
尝试传递指定元素,如果此时另一个线程正在等待接收元素,那么传递成功之后返回,否则阻塞该线程,直到另一个接收元素的线程来匹配!
如果线程在阻塞等待时被中断,则返回并抛出InterruptedException;如果指定元素为 null,那么抛出NullPointerException。
大概步骤为:
- e的null检测,如果e为null,那么抛出NullPointerException异常;
- 调用transferer.transfer方法 判断返回值是否为null,如果为null,因此先将中断标志位恢复为false,随后抛出InterruptedException异常。如果不为null,那么方法正常结束。
无论是公平还是非公平模式,put方法的逻辑都是一样的,只是不同模式调用自己的transfer方法实现,传递参数:
- e – e ,表示尝试传递数据;
- timed – false,表示非超时操作;
- nanos – 0,表示超时时间为0纳秒,由于不是超时操作,这个参数没啥意义。
put方法表示一个尝试传递数据的非超时操作的生产者请求,如果返回值不为null,表示传递成功,返回值就是传递的数据;返回值为null,则说明一定是在等待过程被中断而取消了请求,随后抛出异常,不存在等待超时的情况!
/**
* 尝试传递指定元素,如果此时另一个线程正在等待接收元素,那么传递成功之后返回,否则阻塞该线程,直到另一个线程接收这个元素!
*
* @param e 指定元素
* @throws InterruptedException 如果在等待时被中断
* @throws NullPointerException 如果指定元素为 null
*/
public void put(E e) throws InterruptedException {
//e的null检测
if (e == null) throw new NullPointerException();
//调用transfer方法,不同的模式调用自己的实现,传递 e、false、0
//即表示这是尝试传递数据的非超时操作的生产者请求
if (transferer.transfer(e, false, 0) == null) {
//transfer的返回值如果为null,表示因为被中断而返回
//那么调用interrupted静态方法重置当前线程的中断状态为false
Thread.interrupted();
//最后抛出InterruptedException异常
throw new InterruptedException();
}
}
2.6.2 offer(e)方法
public boolean offer(E e)
尝试传递指定元素,如果此时另一个线程正在等待接收元素,那么传递成功之后返回true,否则立即返回false,不会阻塞该线程!
如果指定元素为 null,那么抛出NullPointerException。
大概步骤为:
- e的null检测,如果e为null,那么抛出NullPointerException异常;
- 调用transferer.transfer方法,判断返回值是否不为null并返回true或者false。
无论是公平还是非公平模式,offer方法的逻辑都是一样的,只是不同模式调用自己的transfer方法实现,传递参数:
- e – e ,表示尝试传递数据;
- timed – false,表示超时操作;
- nanos – 0,表示超时时间为0纳秒,表示offer方法是一个立即返回的方法。
offer(e)方法表示一个尝试传递数据的超时操作的超时时间为0的生产者请求,根据我们对于transer方法的解析,进入循环时如果发现能匹配,那么就是去匹配并且返回传递的元素,在offer方法中返回true;如果发现不能匹配,那么就是尝试等待,在调用awaitFulfill方法之前,都会有一个判断:(timed && nanos <= 0),此时将会满足该条件,因此不必阻塞直接返回null,在offer方法中返回false!
/**
* 尝试传递指定元素
*
* @param e 指定元素
* @return 如果此时另一个线程正在等待接收元素,那么传递成功之后返回true,否则立即返回false,不会阻塞该线程!
* @throws NullPointerException 如果指定元素为 null
*/
public boolean offer(E e) {
//e的null检测
if (e == null) throw new NullPointerException();
//调用transfer方法,不同的模式调用自己的实现,传递 e、true、0
//即表示这是一个尝试传递数据的超时操作的生产者请求,并且超时时间为0纳秒,这实际上就是相当于立即返回的操作
//返回值不为null,则表示传递成功,返回true,否则返回false
return transferer.transfer(e, true, 0) != null;
}
2.6.3 offer(e, timeout, unit)方法
public boolean offer(E e, long timeout, TimeUnit unit)
尝试传递指定元素,传递成功则返回ture,否则阻塞该线程指定时间,超过指定时间还没有传输成功则返回false。
如果线程在阻塞等待时被中断,则返回并抛出InterruptedException;如果指定元素为 null,那么抛出NullPointerException。
大概步骤为:
- e的null检测,如果e为null,那么抛出NullPointerException异常;
- 调用transferer.transfer方法,判断返回值是否不为null,如果不为null,那么表示传递成功,返回true;
- 否则,判断线程是否被中断,并重置中断状态,如果没有被中断,表示因为超时自动返回,那么返回null;
- 如果被中断,表示因为被中断自动返回,那么抛出InterruptedException。
无论是公平还是非公平模式,offer方法的逻辑都是一样的,只是不同模式调用自己的transfer方法实现,传递参数:
- e – e ,表示尝试传递数据;
- timed – false,表示超时操作;
- nanos – unit.toNanos(timeout),表示超时时间纳秒。
offer(e, timeout, unit)方法表示一个尝试传递数据的超时操作的超时时间为指定值的生产者请求,根据我们对于transer方法的解析,进入循环时如果发现能匹配,那么就是去匹配并且返回传递的元素,在offer方法中返回true;如果发现不能匹配,那么就是尝试等待,在调用awaitFulfill方法之前,会有一个判断:(timed && nanos <= 0),此时如果满足该条件将返回false!否则进入awaitFulfill方法等待指定时间,如果此过程中被匹配了,那么也返回true,否则如果是因为超时时间到了就返回false,如果因为被中断就抛出异常!
/**
* 尝试传递指定元素
*
* @return 传递成功则返回ture,否则阻塞该线程指定时间,超过指定时间还没有传输成功则返回false。
* @throws InterruptedException 线程在阻塞等待时被中断
* @throws NullPointerException 指定元素为 null
*/
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
//e的null检测
if (e == null) throw new NullPointerException();
//调用transfer方法,不同的模式调用自己的实现,传递 e、true、指定时间纳秒值
//即表示这是一个尝试传递数据的超时操作的生产者请求,并且超时时间为指定时间
if (transferer.transfer(e, true, unit.toNanos(timeout)) != null)
//返回值不为null,则表示传递成功,返回true
return true;
//返回值为null,则表示传递失败,判断是什么原因
//调用interrupted方法判断线程是否被中断,并重置中断状态
if (!Thread.interrupted())
//如果没有被中断,表示因为超时自动返回,那么返回null
return false;
//如果被中断,表示因为被中断自动返回,那么抛出InterruptedException
throw new InterruptedException();
}
2.6.4 add(e)方法
public boolean add(E e)
尝试传递指定元素,如果此时另一个线程正在等待接收元素,那么传递成功之后返回true,否则立即返回并抛出IllegalStateException!
实际上内部就是调用的offer方法,根据offer方法的返回值判断是否需要抛出异常!add方法仅仅是AbstractQueue的实现,不推荐使用!
/**
* 尝试传递指定元素,如果此时另一个线程正在等待接收元素,那么传递成功之后返回true,否则立即返回并抛出IllegalStateException!
*
* @param e 指定元素
* @return 如果此 collection 由于调用而发生更改,则返回 true
* @throws IllegalStateException 如果此时由于容量限制不能添加元素
* @throws ClassCastException 如果指定元素的类不允许将该元素添加到此队列中
* @throws NullPointerException 如果指定元素为 null 并且此队列不允许 null 元素
* @throws IllegalArgumentException 如果此元素的某些属性不允许将该元素添加到此队列中
*/
public boolean add(E e) {
//实际上调用的offer操作
if (offer(e))
//如果offer成功则返回true
return true;
else
//offer失败则抛出IllegalStateException
throw new IllegalStateException("Queue full");
}
2.7 出队操作
2.7.1 take()方法
public E take()
尝试接收元素,如果此时另一个线程正在等待传递元素,那么接收成功之后返回,否则阻塞该线程,直到另一个传递元素线程的线程来匹配!
如果线程在阻塞等待时被中断,则返回并抛出InterruptedException。
大概步骤为:
- 调用transferer.transfer方法,获取返回值e。如果e不为null,那么返回e。
- 如果e为null,表示因为被中断而返回,调用interrupted静态方法重置当前线程的中断状态为false,最后抛出InterruptedException异常。
无论是公平还是非公平模式,take方法的逻辑都是一样的,只是不同模式调用自己的transfer方法实现,传递参数:
- e – null,表示尝试接收数据;
- timed – false,表示非超时操作;
- nanos – 0,表示超时时间为0纳秒,由于不是超时操作,这个参数没啥意义。
take方法表示一个尝试接收数据的非超时操作的消费者请求,如果返回值不为null,表示传递成功,返回值就是传递的数据;返回值为null,则说明一定是在等待过程被中断而取消了请求,随后抛出异常,不存在等待超时的情况!
/**
* 尝试接收元素,如果此时另一个线程正在等待传递元素,那么接收成功之后返回,否则阻塞该线程,直到另一个传递元素线程的线程来匹配!
*
* @return 接收到的元素
* @throws InterruptedException 线程在阻塞等待时被中断
*/
public E take() throws InterruptedException {
//调用transfer方法,不同的模式调用自己的实现,传递 null、false、0
//即表示这是尝试接收数据的非超时操作的消费者请求,获取返回值e
E e = transferer.transfer(null, false, 0);
//如果e不为null,那么返回e
if (e != null)
return e;
//的返回值如果为null,表示因为被中断而返回
//那么调用interrupted静态方法重置当前线程的中断状态为false
Thread.interrupted();
//最后抛出InterruptedException异常
throw new InterruptedException();
}
2.7.2 poll()方法
public E poll()
尝试接收元素,如果此时另一个线程正在等待传递元素,那么接收成功之后返回接收到的元素,否则立即返回null,不会阻塞该线程!
无论是公平还是非公平模式,poll方法的逻辑都是一样的,只是不同模式调用自己的transfer方法实现,传递参数:
- e – null,表示尝试接收数据;
- timed – false,表示超时操作;
- nanos – 0,表示超时时间为0纳秒,表示offer方法是一个立即返回的方法。
poll()方法表示一个尝试接收数据的超时操作的超时时间为0的消费者请求,根据我们对于transer方法的解析,进入循环时如果发现能匹配,那么就是去匹配并且返回传递的元素;如果发现不能匹配,那么就是尝试等待,在调用awaitFulfill方法之前,都会有一个判断:(timed && nanos <= 0),此时将会满足该条件,因此不必阻塞直接返回null!
/**
* 尝试接收元素
*
* @return 如果此时另一个线程正在等待传递元素,那么接收成功之后返回接收到的元素,否则立即返回null,不会阻塞该线程!
*/
public E poll() {
//调用transfer方法,不同的模式调用自己的实现,传递 null、true、0
//即表示这是一个尝试接收数据的超时操作的消费者请求,并且超时时间为0纳秒,这实际上就是相当于立即返回的操作
return transferer.transfer(null, true, 0);
}
2.7.3 poll(timeout, unit)方法
public E poll(long timeout, TimeUnit unit)
尝试接收元素,接收成功则返回接收到的元素,否则阻塞该线程指定时间,超过指定时间还没有接收成功则返回null。
如果线程在阻塞等待时被中断,则返回并抛出InterruptedException。
大概步骤为:
- 调用transferer.transfer方法,获取返回值e。如果e不为null或者线程没有被中断,那么返回e。
- e为null并且线程被中断,表示因为被中断而返回,调用interrupted静态方法重置当前线程的中断状态为false,最后抛出InterruptedException异常。
无论是公平还是非公平模式,poll方法的逻辑都是一样的,只是不同模式调用自己的transfer方法实现,传递参数:
- e – null,表示尝试接收数据;
- timed – false,表示超时操作;
- nanos – unit.toNanos(timeout),表示超时时间纳秒。
poll(timeout, unit)方法表示一个尝试接收数据的超时操作的超时时间为指定值的消费者请求,根据我们对于transer方法的解析,进入循环时如果发现能匹配,那么就是去匹配并且返回传递的元素;如果发现不能匹配,那么就是尝试等待,在调用awaitFulfill方法之前,会有一个判断:(timed && nanos <= 0),此时如果满足该条件将返回null!否则进入awaitFulfill方法等待指定时间,如果此过程中被匹配了,那么也返回传递的元素,否则如果是因为超时时间到了就返回null,如果因为被中断就抛出异常!
/**
* 尝试接收元素
*
* @return 接收成功则返回接收到的元素,否则阻塞该线程指定时间,超过指定时间还没有接收成功则返回null。
* @throws InterruptedException 如果线程在阻塞等待时被中断
*/
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
//调用transfer方法,不同的模式调用自己的实现,传递null、true、指定时间纳秒值
//即表示这是一个尝试接收数据的超时操作的消费者请求,并且超时时间为指定时间
E e = transferer.transfer(null, true, unit.toNanos(timeout));
//如果e不为null 或者线程没有被中断
if (e != null || !Thread.interrupted())
//返回值e
return e;
//否则表示线程被中断,抛出InterruptedException异常
throw new InterruptedException();
}
2.7.4 remove()方法
public E remove()
尝试接收元素,如果此时另一个线程正在等待传递元素,那么接收成功之后返回接收到的元素,否则立即返回并抛出NoSuchElementException,不会阻塞该线程!
内部实际上就是调用的poll方法,根据poll方法的返回值判断是否需要抛出异常!remove方法仅仅是AbstractQueue的实现,不推荐使用!
/**
* 尝试接收元素
*
* @return 如果此时另一个线程正在等待传递元素,那么接收成功之后返回接收到的元素,否则立即返回并抛出NoSuchElementException,不会阻塞该线程
* @throws NoSuchElementException 没有接收成功
*/
public E remove() {
//直接调用poll方法,获取返回值x
E x = poll();
//如果x不为null,那么返回x;否则抛出NoSuchElementException异常
if (x != null)
return x;
else
throw new NoSuchElementException();
}
2.8 无意义操作
由于SynchronousQueue并不存储元素,因此某些检查、指定操作元素、批量操作元素等方法就是没有意义的,这样的方法非常多:
方法 | 介绍 |
public boolean isEmpty() | 队列是否为空,永远返回true; |
public int size() | 获取元素数量,永远返回0; |
public int remainingCapacity() | 获取剩余容量,永远返回0; |
public void clear() | 移除全部元素,什么也不做; |
public boolean contains(Object o) | 是否包含指定元素o,永远返回false; |
public boolean remove(Object o) | 移除指定元素o,永远返回false; |
public boolean containsAll(Collection<?> c) | 是否包含指定集合c的全部元素,如果c是空的就返回true,否则返回false; |
public boolean removeAll(Collection<?> c) | 移除此集合中那些也包含在指定集合中的所有元,永远返回false; |
public boolean retainAll(Collection<?> c) | 移除此集合中未包含在指定集合中的所有元素,永远返回false; |
public E peek() | 获取但不移除此队列的头,永远返回null; |
public Iterator iterator() | 返回此集合的iterator迭代器,将会返回一个空iterator迭代器; |
public Spliterator spliterator() | 返回此集合的spliterator迭代器,将会返回一个空spliterator迭代器 |
public Object[] toArray() | 返回包含此集合中所有元素的数组,将会返回一个空数组。 |
public T[] toArray(T[] a) | 返回包含此集合中所有元素的数组;返回数组的运行时类型与指定数组的运行时类型相同。如果指定的数组容量大于0,则入参数组并且0索引位置置为null。否则,直接返回入参数组。 |
方法源码:
/**
* 队列是否为空,永远返回true;
*/
public boolean isEmpty() {
return true;
}
/**
* 获取元素数量,永远返回0;
*/
public int size() {
return 0;
}
/**
* 获取剩余容量,永远返回0;
*/
public int remainingCapacity() {
return 0;
}
/**
* 移除全部元素,什么也不做;
*/
public void clear() {
}
/**
* 是否包含o,永远返回false;
*/
public boolean contains(Object o) {
return false;
}
/**
* 移除指定元素,永远返回false;
*/
public boolean remove(Object o) {
return false;
}
/**
* 是否包含指定集合c的全部元素
*
* @param c 指定集合
* @return 如果c是空的就返回true,否则返回false
*/
public boolean containsAll(Collection<?> c) {
return c.isEmpty();
}
/**
* 移除指定集合c的全部元素,永远返回false
*/
public boolean removeAll(Collection<?> c) {
return false;
}
/**
* 移除此集合中未包含在指定集合c中的所有元素,永远返回false;
*/
public boolean retainAll(Collection<?> c) {
return false;
}
/**
* 获取但不移除此队列的头,永远返回null
*/
public E peek() {
return null;
}
/**
* 返回此集合的iterator迭代器,将会返回一个空iterator迭代器
*/
public Iterator<E> iterator() {
return Collections.emptyIterator();
}
/**
* 返回此集合的spliterator迭代器,将会返回一个空spliterator迭代器
*/
public Spliterator<E> spliterator() {
return Spliterators.emptySpliterator();
}
/**
* 返回包含此集合中所有元素的数组,将会返回一个空数组。
*/
public Object[] toArray() {
return new Object[0];
}
/**
* 返回包含此集合中所有元素的数组;返回数组的运行时类型与指定数组的运行时类型相同。
* 如果指定的数组容量大于0,则入参数组并且0索引位置置为null。否则,直接返回入参数组。
*
* @throws NullPointerException 如果指定数组为null
*/
public <T> T[] toArray(T[] a) {
if (a.length > 0)
a[0] = null;
return a;
}
3 SynchronousQueue的总结
SynchronousQueue中并没有专门用来存储元素的容器,内部的双重栈或者双重队列被用来存储等待状态的请求线程。公平模式将会使用双重队列结构(单向链表实现),遵循FIFO的顺序;而非公平模式则使用双重栈结构(单向链表实现),遵循FILo的顺序。
SynchronousQueue内部使用了volatile属性+CAS操作来保证线程安全,以及LockSupport 的 park和unpark方法控制线程的组合和唤醒,并没有使用锁,因此SynchronousQueue非常适合高并发的环境。
SynchronousQueue的put和take请求都必须要等待一个匹配的请求才能返回,这适用于需要同步回调机制的接口,即如果另一请求传递或者接收了数据,那么另外一个等待的请求也同时返回。
Java线程池中,当任务超出了核心线程数量时,会尝试将任务添加到任务队列中,当任务队列满了的时候,就是启用额外的超过核心线程数量的线程。Executors.newCachedThreadPool()返回的预置线程池内部就使用了SynchronousQueue作为任务队列,因此这个队列内部不能存储任务,并且核心线程数量为0,即每来一个任务时在判断如果此时没有空闲的线程之后就会直接会启用新线程去执行该任务,任务执行完毕之后,线程会等待60秒,如果60秒之后没有新任务那么该线程会被回收,这就是可变线程数量线程的线程池原理。
另外,Java的webkit包下的SocketStreamHandle类内部的线程池也是使用了SynchronousQueue作为任务队列。
SynchronousQueue的实现利用了不能存储元素的特性,非常巧妙的将传统的“入队”和“出队”操作合二为一,底层均使用一个transfer方法实现,transfer方法不会区分“入队”和“出队”操作,实际上它就是代表两个匹配的操作之间传递数据的过程,那么自然一组“入队”和“出队”的方法调用都可以返回,而其他的方法则是无意义的方法。SynchronousQueue的源码比较少,实现的非常精炼,主要是理解Doug Lea的思想,值得一读!
相关文章:
- LockSupport:JUC—LockSupport以及park、unpark方法底层源码深度解析
- volatile:Java中的volatile实现原理深度解析以及应用。
- CAS:Java中的CAS实现原理解析与应用。
- UNSAFE:JUC—Unsafe类的原理详解与使用案例。
如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!