生活
Don’t worry if it doesn’t work right. If everything did, you’d be out of a job. (Mosher’s Law of Software Engineering)
不要担心它能否正常工作。如果一切正常,那么你就会失去工作。
前言
昨天一天研究了ArrayBlockingQueue和LinkedBlockingQueue,实现原理都是通过AQS的ReentrantLock来实现的。
今天看了无锁且没有容量没有队列的这个变态的锁:SynchronousQueue。
他的特性就是没有容器,也没有锁:
存数据线程在到达队列时,若发现没有取数据的线程,就在那里一直等待,一直等待有取数据的线程过来唤醒,取走数据,才会释放。反之亦然。
整个实现都是通过CAS实现。
他的源码真的是非常的不好阅读,主要本人涉及到栈、链表的结构的操作不是很熟悉,对照着其他大牛的分析,一步步走Debug来研读,大致摸清了思路。
下面来分析一下源码
SynchronousQueue的创建
先来看下构造器
// 默认创建非公平
public SynchronousQueue() {
this(false);
}
//指定fair为true创建公平
public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue() : new TransferStack();
}
//公平 TransferQueue 队列 先入先出
//不公平 TransferStack 栈结构 后入先出
这里的先入先出,指的不是队列中的元素先入先出(毕竟这个对了的容量为0),指的是 有两个存数据线程等待,进来一个取数据线程来取数据时,取得是第一个等待的线程,这个叫公平,先入先出。
TransferQueue 中的节点对象:
static final class QNode {
volatile QNode next; //下一个节点
volatile Object item; //数据
volatile Thread waiter; // 当前等待的线程
final boolean isData; //是存还是取 isData=true 存
.......
}
TransferStack的节点对象:
static final int REQUEST = 0;
static final int DATA = 1;
static final int FULFILLING = 2;
static final class SNode {
volatile SNode next; // 下一个节点
volatile SNode match; //匹配的节点
volatile Thread waiter; // 等待的线程
Object item; // 数据
int mode; //模式 REQUEST 消费者 DATA 生产者 FULFILLING 匹配模式
}
公平模式源码解析
存取共用一套代码,设计的很巧妙
Object transfer(Object e, boolean timed, long nanos) {
QNode s = null; // constructed/reused as needed
//判断是存还是取
boolean isData = (e != null);
for (;;) {
//拿到头结点和尾节点
//这两个节点在创建队列时已经初始化,且item=null isData=false,t=h
QNode t = tail;
QNode h = head;
// 没有初始化就重来
if (t == null || h == null) // saw uninitialized value
continue; // spin
//如果是初始化 或者 尾节点跟新节点一个类型就往里走,里面有让其等待的方法
if (h == t || t.isData == isData) { // empty or same-mode
//取到尾节点的下一个节点
QNode tn = t.next;
//如果刚才取到的节点已经不是尾节点了,就说明有线程已经插入了一个新节点,那就自旋
if (t != tail) // inconsistent read
continue;
//如果刚才取到的尾节点已经有下一个节点,同样说明有线程已经插入了,那就尝试把尾节点更新并自旋
if (tn != null) { // lagging tail
advanceTail(t, tn);
continue;
}
//超时就直接返回了
if (timed && nanos <= 0) // can't wait
return null;
//第一次自旋的时候没有创建节点就创建一下
if (s == null)
s = new QNode(e, isData);
// CAS设置刚才取到的尾节点的下一个节点为这个新节点,失败就自旋
if (!t.casNext(null, s)) // failed to link in
continue;
//更新尾节点为当前的s节点
advanceTail(t, s); // swing tail and wait
//去匹配
//匹配成功,返回数据
//超时或者中断了返回源节点
Object x = awaitFulfill(s, e, timed, nanos);
//如果取消了,就直接把这个节点清理掉
if (x == s) { // wait was cancelled
clean(t, s);
return null;
}
//如果s没有离队
if (!s.isOffList()) { // not already unlinked
//就设置它为头结点
advanceHead(t, s); // unlink if head
//并且设置s.item=s
if (x != null) // and forget fields
s.item = s;
s.waiter = null;
}
return (x != null) ? x : e;
} else {
//当不是初始化,且与尾节点状态不一致说明进去就能匹配到了 // complementary-mode
//把不算头结点的第一个节点拿出来,头结点不管
QNode m = h.next;
// node to fulfill
//如果之前取得尾节点不是现在的尾节点,或者头结点没有下一个节点,或者之前取得头结点不是现在的头结点,都说明已经有其他线程操作了
if (t != tail || m == null || h != head)
continue; // inconsistent read
//取到数据
Object x = m.item;
//如果状态一直说明已经被匹配了
//如果取出来的数据=m说明 取消了
//cas设置失败的话 都continue
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;
}
}
}
下面来看下具体匹配的方法
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.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);
}
}
看了半天才看懂作者的思路,下面来简单的总结一下:
存数据:
如果当前队列为空或者有存数据线程的节点,就尝试入队,并自旋一定次数后进入等待,直接被另一个取数据线程唤醒,取走数据,并清空存数据的节点,设置为头结点。
如果当前队列中有取数据线程的节点就无需入队,通过CAS设置这个取数据线程节点对象的item,并唤醒这个线程。
反之亦然。
以上只讲解了我认为比较重要的关键方法,其他方法都比较简单,可以自己去看下。
非公平模式源码解析
Object transfer(Object e, boolean timed, long nanos) {
SNode s = null; // constructed/reused as needed
//判断是生产还是消费,如果传入的e是空的就是消费,反之是生产
int mode = (e == null) ? REQUEST : DATA;
for (;;) {
//取到头结点
SNode h = head;
//如果头结点是空或者模式一致就往里走
if (h == null || h.mode == mode) { // empty or same-mode
//如果超时了
if (timed && nanos <= 0) { // can't wait
//如果头结点不为空且被取消,就把头结点出栈
if (h != null && h.isCancelled())
casHead(h, h.next); // pop cancelled node
else
return null;
}
//使新节点入栈
else if (casHead(h, s = snode(s, e, h, mode))) {
//尝试去匹配
//匹配成功 返回匹配成功的节点
//取消了就返回本身
SNode m = awaitFulfill(s, timed, nanos);
//取消了,就把这个节点清理掉
if (m == s) { // wait was cancelled
clean(s);
return null;
}
//成功就是把头结点pop出去
if ((h = head) != null && h.next == s)
casHead(h, s.next); // help s's fulfiller
return (mode == REQUEST) ? m.item : s.item;
}
}
//如果不是匹配模式
else if (!isFulfilling(h.mode)) { // try to fulfill
//先判断头结点有没有取消,取消就出栈
if (h.isCancelled()) // already cancelled
casHead(h, h.next); // pop and retry
//CAS 设置新节点 以匹配模式入栈
else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
for (;;) { // loop until matched or waiters disappear
SNode m = s.next; // m is s's match
//如果下一个节点是空的,就使其出栈
if (m == null) { // all waiters are gone
casHead(s, null); // pop fulfill node
s = null; // use new node next time
break; // restart main loop
}
//
SNode mn = m.next;
//m跟s匹配成功,即刚才入栈的匹配模式的节点与之前的头结点匹配成功,就把这两个都出栈
if (m.tryMatch(s)) {
casHead(s, mn); // pop both s and m
return (mode == REQUEST) ? m.item : s.item;
} else
//否则往后匹配 // lost match
s.casNext(m, mn); // help unlink
}
}
} else {
//如果是匹配模式 // help a fulfiller
SNode m = h.next;
//如果下一个节点是null,就pop出去 // m is h's match
if (m == null) // waiter is gone
casHead(h, null); // pop fulfilling node
else {
//与前面一样,,不说了。。。。
SNode mn = m.next;
if (m.tryMatch(h)) // help match
casHead(h, mn); // pop both h and m
else // lost match
h.casNext(m, mn); // help unlink
}
}
}
}
再来看下 具体匹配的方法:
SNode awaitFulfill(SNode s, boolean timed, long nanos) {
//超时
long lastTime = timed ? System.nanoTime() : 0;
//取到当前线程
Thread w = Thread.currentThread();
//取到头结点。。这个没用到。。是不是大牛写代码 手抖写了上去,,其实就是s。。所以没必要
SNode h = head;
//自旋次数
int spins = (shouldSpin(s) ?
(timed ? maxTimedSpins : maxUntimedSpins) : 0);
for (;;) {
//被打断就取消
if (w.isInterrupted())
s.tryCancel();
//取到匹配的节点就返回
SNode m = s.match;
if (m != null)
return m;
//超时
if (timed) {
long now = System.nanoTime();
nanos -= now - lastTime;
lastTime = now;
if (nanos <= 0) {
s.tryCancel();
continue;
}
}
自旋
if (spins > 0)
spins = shouldSpin(s) ? (spins-1) : 0;
else if (s.waiter == null)
s.waiter = w; // establish waiter so can park next iter
else if (!timed)
LockSupport.park(this);
else if (nanos > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanos);
}
}
简单总结一下:
存数据:
如果当前等待栈为空或者前面的节点的模式也是存,就尝试入栈,自旋一定次数进入等待状态,直到被唤醒,唤醒匹配成功后把自己和匹配的节点都出栈。
如果头结点的模式是取,就判断当前节点是不是匹配模式,如果不是,就封装为匹配模式,尝试匹配,匹配成功,两个节点都出栈。