JUC源码分析-容器-ConcurrentLinkedQueue

概述

ConcurrentLinkedQueue 是单向链表结构的无界并发队列。从JDK1.7开始加入到J.U.C的行列中。使用CAS实现并发安全,元素操作按照 FIFO (first-in-first-out 先入先出) 的顺序。适合“单生产,多消费”的场景。

非阻塞算法

在 Michael-Scott (麦克尔,斯科特 )非阻塞队列算法上进行一下修改

实现了 松弛阀值 即惰性更新的策略,容忍了 tail 不能及时更新, 因不需要及时更新tail 算法中也减少了 其他线程 帮助 处理中间状态 (参考资料中讲解 中间状态)的步骤

下面列出 ConcurrentLinkedQueue 非阻塞算法要点

  1. 使用 CAS 原子指令来处理对数据的并发访问,这是非阻塞算法得以实现的基础,
  2. CAS执行前 需要前置条件。
  3. 惰性更新 head tail
  4. 性能优化:head tail 发生变化 及时获取,减少遍历次数
  5. 使用“自链接”方式管理出队节点,这样一个自链接节点意味着需要从head向后推进。

 

源码分析

offer

方法逻辑如下:

向队列写入节点,先创建新的节点, 从尾部开始循环查找,如果发现尾部没有节点 ,连接到

尾部,否则检查尾部节点是否自链接,否则如果tail节点更新获取tail结点,没有更新获取下个节点检查是否可以链接。

public boolean offer(E e) {

checkNotNull(e);

final Node newNode = new Node(e);

 

for (Node t = tail, p = t;;) {

Node q = p.next;

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的快照指向,这种写法即获取了改变后的对象,又比较了对象是否改变。

poll

方法逻辑如下:

从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 h = head, p = h, q;;) {

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

 

你可能感兴趣的:(JUC源码分析)