文章目录
1.数据结构
1.等待队列:waitQueue
waitQueue是一个接口,在这个队列当中会声明两个等待的队列,分别是生产者和消费者
static class WaitQueue implements java.io.Serializable { }
//生产者队列
private WaitQueue waitingProducers;
//消费者队列
private WaitQueue waitingConsumers;
1.公平队列:FifoWaitQueue
公平队列对应的是先进先出,对应的数据结构的queue
static class FifoWaitQueue extends WaitQueue {
private static final long serialVersionUID = -3623113410248163686L;
}
2.非公平队列:LifoWaitQueue
非公平队列对应的是后进先出,对应的数据结构是stack
static class LifoWaitQueue extends WaitQueue {
private static final long serialVersionUID = -3633113410248163686L;
}
2.数据传输方式:Transferer
数据传输方式,这边定义了一个抽象方法transfer,Performs a put or take.
,是用来处理拿出和放入操作的.
/**
* Shared internal API for dual stacks and queues.
*/
abstract static class Transferer<E> {
/**
* Performs a put or 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);
}
这边分别对应着两种不同的传输方式:TransferStack、TransferQueue
1.TransferStack
TransferStack包含一个Snode节点,这边不直接看方法,看到队列操作之后再根据具体详细分析
static final class SNode {
volatile SNode next; // next node in stack
volatile SNode match; // the node matched to this
volatile Thread waiter; // to control park/unpark
Object item; // data; or null for REQUESTs
int mode;
}
2.TransferQueue
TransferQueue包含一个Qnode节点,这边不直接看方法,看到队列操作之后再根据具体详细分析
TransferQueue() {
QNode h = new QNode(null, false); // initialize to dummy node.
head = h;
tail = h;
}
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;
}
2.方法说明
1.转换(重点):transfer
在这个类型队列的所有操作中基本都用到了transfer方法,主要的区别是是否计时,后面会讲到。这边只查看stack的实现:
这一段代码比较长,这边我们分成四个部分来说,下面是代码的原貌。后面我们一个一个部分来理解.
E transfer(E e, boolean timed, long nanos) {
// constructed/reused as needed
SNode s = null;
//当e==null的时候说明是消费者进来等待了(REQUEST),否则表示生产者(DATA)生产了等待消费
int mode = (e == null) ? REQUEST : DATA;
for (; ; ) {
//拿到头结点
SNode h = head;
// empty or same-mode
//如果头结点为空,或者也是相同的模式(比如都是消费者)
if (h == null || h.mode == mode) {
// can't wait
//时间到了
if (timed && nanos <= 0) {
// 如果头节点不为空且是取消状态
if (h != null && h.isCancelled()) {
// pop cancelled node
//把头结点弹出
casHead(h, h.next);
} else {
return null;
}
}
//cas会进行入栈,这边是入栈成功了
else if (casHead(h, s = snode(s, e, h, mode))) {
//睡眠,如果是超时或者是中断,会把match替换成this(自己)尝试取消
SNode m = awaitFulfill(s, timed, nanos);
//返回节点的match是自己,取消
if (m == s) {
// wait was cancelled
//清除当前节点
clean(s);
return null;
}
//匹配到元素:因为从awaitFulfill()里面出来要不被取消了要不就匹配到了
//如果头结点不为空,并且s是头结点下一个节点,弹出head和s.
if ((h = head) != null && h.next == s) {
// help s's fulfiller
casHead(h, s.next);
}
//根据模式返回生产者还是消费者的值
return (E) ((mode == REQUEST) ? m.item : s.item);
}
// try to fulfill
}
//如果h.mode不是FULFILLING,并且不是相同的模式
else if (!isFulfilling(h.mode)) {
// already cancelled
//节点取消
if (h.isCancelled()) {
// pop and retry
//头结点换成下一个节点
casHead(h, h.next);
}
//这边应该是头结点 不是cancelled的,s入栈变成头结点
else if (casHead(h, s = snode(s, e, h, FULFILLING | mode))) {
// loop until matched or waiters disappear
for (; ; ) {
// m is s's match
SNode m = s.next;
// all waiters are gone
//如果没有下一个节点了,清空栈跳出循环继续往下走
if (m == null) {
// pop fulfill node
casHead(s, null);
// use new node next time
s = null;
// restart main loop
break;
}
//如果头结点的下一个节点不为空,往再下一个节点找
SNode mn = m.next;
//这个地方的条件是是两个可以匹配的节点(生产和消费)
if (m.tryMatch(s)) {
//匹配成功,两个节点都弹出
// pop both s and m
casHead(s, mn);
return (E) ((mode == REQUEST) ? m.item : s.item);
// lost match
} else {
//尝试匹配失败,说明m已经先一步被其它线程匹配了,清除m节点
// help unlink
s.casNext(m, mn);
}
}
}
// help a fulfiller
}
//如果不是相同模式,并且当前节点是FULFILLING的情况
else {
//mode在上面只可能是生产或者消费,这边的话说明都是FULFILLING的情况
// m is h's match
SNode m = h.next;
// waiter is gone
if (m == null) {
//头结点的下一个节点为null
// pop fulfilling node
casHead(h, null);
} else {
//否则尝试匹配.
SNode mn = m.next;
// help match
//匹配成功,两个节点都弹出
if (m.tryMatch(h)) {
// pop both h and m
casHead(h, mn);
}
// lost match
else {
// help unlink
//尝试匹配失败,说明m已经先一步被其它线程匹配了,清除m节点
h.casNext(m, mn);
}
}
}
}
}
1.区分操作模式:mode
这边说的模式是相对于线程操作来说的,这边定义的三种模式:REQUEST(消费)、DATA(生产)、FULFILLING(交换),第一部分是怎么判断这个模式:int mode = (e == null) ? REQUEST : DATA;
这边的话,根据入参,如果传入的是null就是要消费,可以看下面的take方法,如果传入的不是null,就是要生产,可以看下面的put,这个应该很好理解。
2.头节点和当前是相同模式
这边的操作步骤分别如下:
先判断是否超时,如果超时的话,判断头部节点不为空,并且被取消(匹配的节点是自己,也就是说没有匹配到可以进行交换的节点),通过cas方式把头部换成下一个节点。如果头部节点为空, 说明没有队列了,直接返回null;
如果没有超时一说,先把当前节点添加到栈中,这边的栈的入栈和以前的入栈可能不一样(后入的节点添加到head,然后出栈的时候出的也是head,实现后进先出),然后下一步调用awaitFulfill方法,这个方法的作用是找到匹配的节点(不同模式),如果没找到自旋结束挂起等待被唤醒,match如果是自己则说明匹配失败(被cancel)
往后判断一下匹配的节点是不是自己,如果是则清除当前节点返回null。否则就是匹配到了节点(因为awaitFulfill方法只有一个出口,出口必定携带匹配的节点值),匹配到之后设置头部节点,根据mode返回相应的节点值(item)
//时间到了
if (timed && nanos <= 0) {
// 如果头节点不为空且是取消状态
if (h != null && h.isCancelled()) {
// pop cancelled node
//把头结点弹出
casHead(h, h.next);
} else {
return null;
}
}
//cas会进行入栈,这边是入栈成功了
else if (casHead(h, s = snode(s, e, h, mode))) {
//睡眠,如果是超时或者是中断,会把match替换成this(自己)尝试取消
SNode m = awaitFulfill(s, timed, nanos);
//返回节点的match是自己,取消
if (m == s) {
// wait was cancelled
//清除当前节点
clean(s);
return null;
}
//匹配到元素:因为从awaitFulfill()里面出来要不被取消了要不就匹配到了
//如果头结点不为空,并且s是头结点下一个节点,弹出head和s.
if ((h = head) != null && h.next == s) {
// help s's fulfiller
casHead(h, s.next);
}
//根据模式返回生产者还是消费者的值
return (E) ((mode == REQUEST) ? m.item : s.item);
}
// try to fulfill
看一下awaitFulfill方法:
这边先是会计算一下循环的次数
然后进入死循环,出口只有一个就是 match!=null的时候,返回match值。
如果被中断和超时的情况下会调用trycancel方法会将当前节点的match值设置为自己。然后下一个循环开始的时候,match不为null就返回了
如果是没有计时的情况,可以看到park
的操作,这边的park是等待下一个操作进来的时候去唤醒这个节点。我们可以往后去看下面的方法
SNode awaitFulfill(SNode s, boolean timed, long nanos) {
final long deadline = timed ? System.nanoTime() + nanos : 0L;
Thread w = Thread.currentThread();
//计算循环的次数,这边会分别根据有计时和没计时进行判断
int spins = (shouldSpin(s) ? (timed ? maxTimedSpins : maxUntimedSpins) : 0);
for (; ; ) {
//被中断,尝试取消这个节点
if (w.isInterrupted()){
// Tries to cancel a wait by matching node to itself.
//1 - 尝试通过将节点与自身匹配来取消等待。
s.tryCancel();
}
//匹配的节点不为空返回
SNode m = s.match;
if (m != null){
return m;
}
//有计时的操作,到达时间取消节点
if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
//2 - 尝试通过将节点与自身匹配来取消等待。
s.tryCancel();
continue;
}
}
//还有自旋次数,计算自旋次数
if (spins > 0){
spins = shouldSpin(s) ? (spins - 1) : 0;
}
//没有自旋次数,设置waiter为当前线程
else if (s.waiter == null){
// establish waiter so can park next iter
s.waiter = w;
}
//没有自旋次数并且不计数,进行park休眠
else if (!timed){
LockSupport.park(this);
}
//如果有计时,则休眠对应时间
else if (nanos > spinForTimeoutThreshold){
LockSupport.parkNanos(this, nanos);
}
}
}
3.头结点和当前当前不是相同模式
如果头节点和当前节点不是相同的模式,这边还有一个条件就是不是辅助模式。也就是说可能进来的节点是消费(take),而头结点是生产(put),那么这两个模式不一样,正好可以抵消了。所以就进入这个判断:
这边会先判断节点是否被取消(就是匹配的等于自己,这个是操作基本是中断和超时导致),头结点被取消就往下一个节点走
否则把当前节点添加到头部(入栈),并且修改状态为交换,开始死循环
如果没有下一个节点,设置头结点为null,跳出循环结束,下一步就会再走到上面的2.头节点和当前是相同模式,可能被挂起等待或者超时结束。
如果下一个节点不为空的情况,进行tryMatch匹配,如果能匹配到出栈(这个时候是两个节点),返回对应的元素
如果匹配不上,说明可能被另外一个节点匹配了,就要往在下一个节点去匹配。
// already cancelled
//节点取消
if (h.isCancelled()) {
// pop and retry
//头结点换成下一个节点
casHead(h, h.next);
}
//这边应该是头结点 不是cancelled的,s入栈变成头结点
else if (casHead(h, s = snode(s, e, h, FULFILLING | mode))) {
// loop until matched or waiters disappear
for (; ; ) {
// m is s's match
SNode m = s.next;
// all waiters are gone
//如果没有下一个节点了,清空栈跳出循环继续往下走
if (m == null) {
// pop fulfill node
casHead(s, null);
// use new node next time
s = null;
// restart main loop
break;
}
//如果头结点的下一个节点不为空,往再下一个节点找
SNode mn = m.next;
//这个地方的条件是是两个可以匹配的节点(生产和消费)
if (m.tryMatch(s)) {
//匹配成功,两个节点都弹出
// pop both s and m
casHead(s, mn);
return (E) ((mode == REQUEST) ? m.item : s.item);
// lost match
} else {
//尝试匹配失败,说明m已经先一步被其它线程匹配了,清除m节点
// help unlink
s.casNext(m, mn);
}
}
}
// help a fulfiller
}
tryMatch如何进行匹配,这边看一下
这边如果匹配成功,成功的前提原来的头结点(当前节点的下一个节点)的match为空(没有被匹配),并且cas操作是从null到当前节点成功,可能失败,就是在这个过程中被其他节点匹配走了,如果成功要唤醒原来的头结点,因为之前可能被park起来等待,现在match不为空了,可以起来工作了,匹配失败返回结果
boolean tryMatch(SNode s) {
//如果m没有匹配这,将s作为他的匹配这
if (match == null && UNSAFE.compareAndSwapObject(this, matchOffset, null, s)) {
Thread w = waiter;
// waiters need at most one unpark
//唤醒m中的waiter
if (w != null) {
waiter = null;
LockSupport.unpark(w);
}
// 返回true
return true;
}
//可能有其他线程匹配了m,返回是否匹配结果
return match == s;
}
4.当前节点是辅助模式(可能从上一步来)
因为之前的第二个判断会把当前节点设值为交换中状态。这边的操作相当于第二个判断之后会进来
操作和上面的操作很类似,如果head的下一个节点为null,也要回到第一个判断在进行操作等待。
否则尝试匹配,成功弹出两个节点,失败继续往下,不过这边成功没有返回的打算,依然会再进入自旋的下一步操作
这边可以理解这一步操作是辅助操作,清除匹配成功的节点,或者当节点所属线程消失后将其移除栈。
//mode在上面只可能是生产或者消费,这边的话说明都是FULFILLING的情况
// m is h's match
SNode m = h.next;
// waiter is gone
if (m == null) {
//头结点的下一个节点为null
// pop fulfilling node
casHead(h, null);
} else {
//否则尝试匹配.
SNode mn = m.next;
// help match
//匹配成功,两个节点都弹出
if (m.tryMatch(h)) {
// pop both h and m
casHead(h, mn);
}
// lost match
else {
// help unlink
//尝试匹配失败,说明m已经先一步被其它线程匹配了,清除m节点
h.casNext(m, mn);
}
}
5.图解
2. 获取一个元素:take
public E take() throws InterruptedException {
//直接调用transfer方法,不计数
E e = transferer.transfer(null, false, 0);
if (e != null){
return e;
}
//如果元素为空,让线程中断抛出中断异常结束.
Thread.interrupted();
throw new InterruptedException();
}
可以看到take这边是直接传入null,表明自己是消费者,并且没有设置超时时间,一直等待到对应的生产者给定数据返回
3.加入队列:put
put这边也不设定时间,相当于是等待消费者消费掉当前操作生产的节点.
public void put(E e) throws InterruptedException {
if (e == null) {
throw new NullPointerException();
}
//把当前节点添加进去执行transfer操作
if (transferer.transfer(e, false, 0) == null) {
//返回为空就中断被排除异常
Thread.interrupted();
throw new InterruptedException();
}
}
3.总结
可以看到虽然说这个队列内部的容量是0,但是他还是会维护一个队列,个人感觉这种队列适合用于短期的任务,生产和消费相近,不会造成大量任务堆积。等后面看完线程池再回来补充吧