Java并发学习:基于CAS非阻塞并发队列源码解析,mybatis面试题

head的不变式与可变式

============

不变式

  1. 所有存活的节点,都能从head通过调用succ()方法遍历可达。

  2. head不能为null。

  3. head节点的next域不能引用到自身。

可变式

  1. head节点的item值可能为null,也可能不为null。

  2. 允许tail之后与head,也就是说:从head开始遍历队列,不一定能达到tail。

tail的不变式与可变式

============

不变式

  1. 通过tail调用succ()方法,最后节点总是可达的。

  2. tail不能为null。

可变式

  1. tail节点的item域可能为null,也可能不为null。

  2. 允许tail滞后于head,也就是说:从head开始遍历队列,不一定能到达tail。

  3. tail节点的next域可以引用到自身。

offer操作

=======

源码解析

====

offer操作将会将元素e【非null】加入到队尾,由于无界队列的特性,这个操作将永远不会返回false。

public boolean offer(E e) {

// 检查元素是否为null,为null就抛空指针

checkNotNull(e);

// 构造新节点

final Node newNode = new Node(e);

// 【1】for循环从tail开始迭代

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

Node q = p.next;

// 【2】q == null 说明是p是尾节点

if (q == null) {

// 【3】

// cas将p的next设置为newNode,返回true

// 如果设置失败,说明有其他线程修改了p.next

// 那就再次进入循环

if (p.casNext(null, newNode)) {

// 【4】

// 这里tail指针并不是每次插入节点都要更改的,从head开始第奇数个节点会是tail

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

}

//【5】

else if (p == q)

// 并发情况下,移除head的时候【比如poll】,将会head.next = head

// 也就满足p == q 的分支条件, 需要重新找到新的head

p = (t != (t = tail)) ? t : head;

//【6】

else

// 表明tail指向的已经不是最后一个节点了,更新p的位置

// 这里其实就是找到最后一个节点的位置

p = (p != t && t != (t = tail)) ? t : q;

}

}

图解offer操作

=========

上面是模拟的单线程情况下的offer一个元素的操作,可以看到:

  1. 初始化head、tail都指向了item为null的哨兵节点,他们的next指向null。

  2. 单线程情况下,我们暂时认为CAS操作都是执行成功的,此时q为null,将会走第一个分支【2】,将p的next指向newNode,此时p==t,因此不会执行【4】casTail操作,直接返回true。

多线程情况下,事情就不是这么简单了:

  1. 加入线程A希望在队尾插入数据A,线程B希望在队尾插入数据B,他们同时到了【3】p.casNext(null, newNode)这一步,由于casNext是原子性的,假设A此时设置成功,且p == t,如图1。

  2. A成功,自然B线程cas设置next失败,那么将会再次进行for循环,此时q != null && p != q,走到【6】,将p移动到q的位置,也就是A的位置,如图2。

  3. 再次循环,此时q==null,再次进行【3】的设置next操作,此时假设B成功了,如图3。

  4. 此时你会发现,tail需要重新设置了,因为p != t条件满足【4】,将会执行casTail(t, newNode),将tail指针指向插入的B。

相信一通图解 + 源码分析下来,你会慢慢对整个流程熟悉起来,稍微总结一下:

offer操作其实就是通过原子CAS操作控制某一时刻只有一个线程能成功在队尾追加元素,CAS失败的线程将会通过循环再次尝试CAS操作,直到成功。 非阻塞算法就是这样,通过循环CAS的方式利用CPU资源来替代阻塞线程的资源消耗。 并且,tail指针并不是每次都是指向最后一个节点,由于自身的机制,最后一个节点要么是tail指向的位置,要么就是它的next。因此定位的时候,这里使用p指针定位最后一个节点的位置。

对了,你会发现,在整个过程中,【5】操作一直没有涉及到,其实【5】的情况会在poll操作的时候可能会发生,这里先举个例子吧:

图一是poll操作可能会导致的情况的一种,以他为例子:此时tail节点指向弃用的节点,此时向队列中offer一个元素。

  1. 此时,执行到【2】处,各个指针的指向如图1。

  2. 接着由于q不为null,且p == q,顺利进入【5】,这时p被赋值为head,如图2。

  3. 再次循环,q指向p.next,此时为null,如图3。

  4. q为null,进入【2】,和之前一样,【3】设置next,此时【4】p != t,设置新节点为新的tail,如图4。

JDK1.6 hops设计意图

===============

在看源码注释的时候,我发现很多处都对hop这个玩意进行了注释,原来JDK1.6的源码中确实有它的存在:聊聊并发(六)ConcurrentLinkedQueue的实现原理分析,并且设计的理念还是一样的,用hops控制tail节点的更新频率,提高入队的效率。

引用《Java并发编程的艺术》方腾飞 : 减少CAS更新tail节点的次数,就能提高入队的效率,所以doug lea使用hops变量来控制并减少tail节点的更新频率,并不是每次节点入队后都将 tail节点更新成尾节点,而是当 tail节点和尾节点的距离大于等于常量HOPS的值(默认等于1)时才更新tail节点,tail和尾节点的距离越长使用CAS更新tail节点的次数就会越少,但是距离越长带来的负面效果就是每次入队时定位尾节点的时间就越长,因为循环体需要多循环一次来定位出尾节点,但是这样仍然能提高入队的效率,因为从本质上来看它通过增加对volatile变量的读操作来减少了对volatile变量的写操作,而对volatile变量的写操作开销要远远大于读操作,所以入队效率会有所提升。

private static final int HOPS = 1;

public boolean offer(E e) {

if (e == null) throw new NullPointerException();

Node n = new Node(e);

retry:

for (;😉 {

Node t = tail;

Node p = t;

for (int hops = 0; ; hops++) {

Node next = succ§; // 1.获取p的后继节点。(如果p的next指向自身,返回head节点)

if (next != null) { // 2.如果next不为null

if (hops > HOPS && t != tail)

continue retry; // 3.如果自旋次数大于HOPS,且t不是尾节点,跳出2层循环重试。

p = next; // 4.如果自旋字数小于HOPS或者t是尾节点,将p指向next。

} else if (p.casNext(null, n)) { // 5.如果next为null,尝试将p的next节点设置为n,然后自旋。

if (hops >= HOPS)

casTail(t, n); // 6.如果设置成功且自旋次数大于HOPS,尝试将n设置为尾节点,失败也没关系。

return true; // 7.添加成功。

} else {

p = succ§; // 8。如果第5步尝试将p的next节点设置为n失败,那么将p指向p的后继节点,然后自旋。

}

}

}

final Node succ(Node p) {

Node next = p.getNext();

//如果p节点的next节点指向自身,那么返回head节点;否则返回p的next节点。

return (p == next) ? head : next;

poll操作

======

poll操作将在队头出队一个元素,并返回,如果队列为空,则返回null。

源码解析

====

public E poll() {

// 【1】continue xxx;会回到这

restartFromHead:

// 【2】死循环

for (;😉 {

for (Node h = head, p = h, q;😉 {

E item = p.item;

// 【3】如果当前 有值, 就cas操作置null

if (item != null && p.casItem(item, null)) {

// Successful CAS is the linearization point

// for item to be removed from this queue.

// 【4】

if (p != h) // hop two nodes at a time

updateHead(h, ((q = p.next) != null) ? q : p);

return item;

}

// 【item == null】 或 【item != null 但是 cas失败了】

// 【5】队列为空, 返回null

else if ((q = p.next) == null) {

updateHead(h, p);

return null;

}

// 【6】

else if (p == q)

continue restartFromHead;

// 【7】

else

p = q;

}

}

小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
img

读者福利

秋招我借这份PDF的复习思路,收获美团,小米,京东等Java岗offer

更多笔记分享

秋招我借这份PDF的复习思路,收获美团,小米,京东等Java岗offer

。**
[外链图片转存中…(img-fVcrh65u-1710970884231)]
[外链图片转存中…(img-rRev1LWB-1710970884232)]
[外链图片转存中…(img-D6LYA5RW-1710970884232)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
[外链图片转存中…(img-aCl3XJdO-1710970884233)]

读者福利

[外链图片转存中…(img-b2UOb0X7-1710970884233)]

更多笔记分享

[外链图片转存中…(img-AAq5GuIm-1710970884234)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值