ConcurrentLinkedQueue 是单向链表结构的无界并发队列。从JDK1.7开始加入到J.U.C的行列中。使用CAS实现并发安全,元素操作按照 FIFO (first-in-first-out 先入先出) 的顺序。适合“单生产,多消费”的场景。
在 Michael-Scott (麦克尔,斯科特 )非阻塞队列算法上进行一下修改
实现了 松弛阀值 即惰性更新的策略,容忍了 tail 不能及时更新, 因不需要及时更新tail 算法中也减少了 其他线程 帮助 处理中间状态 (参考资料中讲解 中间状态)的步骤
下面列出 ConcurrentLinkedQueue 非阻塞算法要点
方法逻辑如下:
向队列写入节点,先创建新的节点, 从尾部开始循环查找,如果发现尾部没有节点 ,连接到
尾部,否则检查尾部节点是否自链接,否则如果tail节点更新获取tail结点,没有更新获取下个节点检查是否可以链接。
public boolean offer(E e) {
checkNotNull(e);
final Node
for (Node
Node
if (q == null) {//队尾无节点
// p is last node
if (p.casNext(null, newNode)) {//链接到队尾
// Successful CAS is the linearization point
// for e to become an element of this queue,
// and for newNode to become "live".
//写入两个节点后,再更新尾部节点
if (p != t) // hop two nodes at a time
//更新尾部节点,由于惰性更新策略,失败也可以,如果失败说明 被其他线程更新过了。
casTail(t, newNode); // Failure is OK.
return true;
}
//链接到队尾失败,被其他线程抢先,重新获取下个节点
// Lost CAS race to another thread; re-read next
}
else if (p == q)
//p是自链接节点
//疑问1:p什么情况下回自链接?
//p 代表尾部,如果t已经修改,赋值t,否则赋值head,从head往后查找真正的tail.
//疑问2:t != (t = tail) 这种写法 的作用
p = (t != (t = tail)) ? t : head;
else
// Check for tail updates after two hops.
//p != t 为false, 执行了 p=q, p指向了下个节点 ,t != (t = tail)为true说明tail发生改变。
//p指向了下个节点并且tail发生变化,重新获取tail,否则获取下个节点,继续寻找真正的tail.
p = (p != t && t != (t = tail)) ? t : q;
}
}
下面分析一下方法中的疑问:
疑问1:结合poll()(后面讲解),head是以指向自己出队然后向队尾移动的。当head和tail相同,并发下此条件会成立。如当head和tail相同, 队尾插入了2个待匹配节点后没有及时更新tail,并发poll 2个节点又被匹配上,出队head,head指向自己,并发的插入线程 发现tail没有更新,执行到此,此条件成立。
疑问2:t != (t = tail) 按照以前的知识理解 t=tail赋值后 t应该发生改变 ,该比较应该等于true, 这里用来判断 tail是否改变 说明 != 前 记录了 t的快照指向,!= 后保存了 t=tail的快照指向,这种写法即获取了改变后的对象,又比较了对象是否改变。
方法逻辑如下:
从head寻找匹配的节点,匹配上了 将被匹配节点item CAS替换为null
如果符合出队两个节点的条件则执行出队,返回获取到的item,没有匹配上
如果有后续待匹配节点,说明匹配不上,返回null。 否则如果检查到被匹配节点是被出队的Head节点,从head开始重新匹配。没有异常情况 找到下个待匹配节点进行匹配
public E poll() {
restartFromHead:
for (;;) {
//弄清这几个变量有助于理解整个逻辑。 h是head的快照, p是变量队列的句柄,开始指向head,尝试匹配时q==p, 未匹配上 q是 p的下个节点。向后遍历执行 p=q;
for (Node
E item = p.item;// code A
//匹配上节点
//匹配后item==null
if (item != null && p.casItem(item, null)) {
if (p != h) // hop two nodes at a time
//上个节点已经被匹配,出队上个节点和当前被匹配的节点,并且h next指向自己
//疑问1:这样出队后会出现 q .next==p q会被回收掉么?
//疑问2:((q = p.next) != null) ? q : p有什么用, head可以修改为null么?
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
else if ((q = p.next) == null) {//没匹配上,q节点next没有待匹配节点
updateHead(h, p);//修改p为头部节点, h next 指向自己
return null;
}
else if (p == q)//q 是被出队的 头节点
//疑问3:直接获取下个next节点,可以么
continue restartFromHead;
else n
p = q;// 找到下个匹配节点
}
}
}
下面分析下代码中的疑问
疑问1:虽然 队列中q .next==p,但是没有变量或对象可以访问到q, 应该可以被回收,待验证。
疑问2:((q = p.next) != null) ? q : p 目前只出队了p,只更新 p为head就可以,这里获取了p的next 结点q,这样第二次poll 中head 节点 就是数据节点,只有第三次执行poll才会更新head节点。实现了惰性更新head策略。
如果head==null 那么 code A就抛出异常了,可以通过检查代码解决,但是 整个poll方法需要修改,增加 初始化 head的部分代码,实现比较复杂,不如在构造方法中初始化head 和tail ,offer和poll方法默认 head 和tail不等于null.
疑问3:此时有可能有并发 队列中节点发生了变化,需要从head开始重新匹配。
当消费速度大于生产速度时head有可能超过tail
总结:ConcurrentLinkedQueue 基于Michael-Scott非阻塞队列算法修改 实现,理解算法是重点。
参考资料:
https://blog.csdn.net/dcm19920115/article/details/91678408