资讯专栏INFORMATION COLUMN

<<Java并发编程实践>>有感 ConcurrentLinkedQueue

LucasTwilight / 3363人阅读

摘要:上集算法实现的优点当一个线程执行任务失败不影响其他线程的进行最大限度的利用资源能提高程序的伸缩性伸缩性不修改任何代码升级硬件就能带来性能上的提高升级硬件带来的性能提高明显就是伸缩性良好的缺点代码复杂影响阅读性刚开始看的时候没有正确的思路理解

ConcurrentLinkedQueue(上集) 算法实现 CAS

CAS的优点

当一个线程执行任务失败不影响其他线程的进行 最大限度的利用CPU资源 能提高程序的伸缩性 伸缩性:不修改任何代码 升级硬件就能带来性能上的提高 升级硬件带来的性能提高明显 就是伸缩性良好

CAS的缺点

代码复杂 影响阅读性 刚开始看ConcurrentLinkedQueue的时候 没有正确的思路,理解起来会比较费劲 我推荐直接用多线程同时执行的方式去理解 这样会比较好

重要概念

不变性

所有item不为null的节点都能从head头节点开始通过succ()方法访问到

head!=null 只要队列有值 保证真实的head永不为null head哪怕会自引用 迟早也会解除这种假状态

可变性

heatd.item 可能为null也可能不为null 因为cas活锁操作 每一行代码执行都不影响其他线程的访问相同的代码块

tail尾节点的更新是滞后于head的 个人理解 在offer中 尾节点掉队后 通过head节点 (不变性1的保证) 成功访问最后一个p.next=null的节点

快照

snapshot是我自己的理解 因为对于多线程操作来说 当前引用对象 如offer()中 t=tail中的t; p=t中的p; q=p.next中的q都是一个快照 他获得一个对象的快照版本 然后在后续的操作中 使(t!=(t=tail))这样操作有意义

重要方法

offer()入队

poll() 出队

源码
public boolean offer(E e) {
        checkNotNull(e); //NullPointException检查   
        final Node newNode = new Node(e); //包装成一个Node对象

        for (Node t = tail, p = t;;) {//获取当前尾节点 t=tail,p是真正的尾节点 p.next==null 
            Node q = p.next;
            if (q == null) {
                // p is last node 
                if (p.casNext(null, newNode)) {//方法1 CAS更新 自己想3个线程同时进行这个操作
                    // 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 //方法2 延迟更新尾节点 下面说为什么
                        casTail(t, newNode);  //方法3 成不成功无所谓 下面说
                    return true;
                }
                // Lost CAS race to another thread; re-read next
            }
            else if (p == q)// 方法4 学习offer方法时 可以暂时放弃这一步
                // We have fallen off list.  If tail is unchanged, it
                // will also be off-list, in which case we need to
                // jump to head, from which all live nodes are always
                // reachable.  Else the new tail is a better bet.
                p = (t != (t = tail)) ? t : head;
            else  //去找到真正的尾节点 此处和方法2 应是相互辉映的存在
                // Check for tail updates after two hops.
                p = (p != t && t != (t = tail)) ? t : q; //方法5
        }
    }
解读offer()方法

自顶向下 思考CAS中可能出现的情况 CAS是活锁 所谓活锁即是每一行代码运行时 允许其他线程访问相同的代码块 成功与失败并存 衍生了更多的条件判断 本人觉得CAS方法都应该从这个方法去理解 再自己画画时序图 (注意:理解offer()时,先把方法4排除,因为4方法出现自引用的情况 只有offer()poll()交替执行时会出现 本文只介绍第一种情况)

多线程操作

第一种情况: 只有 offer()

第二种情况: offer()poll()方法交替执行

同时执行offer()(假设我们现在有3个线程)

不变性:永远只有一个线程CAS成功 并且总会成功一个

循环次数分析:Thread1 成功 循环一次退出 Thread2失败 再循环一次成功 Thread3失败 再循环两次成功 如果有n个线程同时执行 offer() 执行次数 最大为n次 最少为1次

方法5中三目表达式解析: p=condition?result1:result2 我先说一下这里的意义 满足result1的场景为 :获取尾节点tail的快照已经过时了(其他线程更新了新的尾节点tail) 直接跳转到当前获得的最新尾节点的地方 满足result2的场景为:多线程同时操作offer() 执行1方法CAS成功后 未更新尾节点(未执行3方法:两种原因 1是未满足前置条件if判断 2是CAS更新失败) 直接找next节点

方法2与方法5 是整个offer() 操作的点睛之笔 下面解释

只有offer() 操作时

假设:

Thread 1执行完1方法成功 还未执行2方法 Thread2和Thread3进入5方法 ,也就是说Thread2和Thread3执行5方法发生在Thread1执行2方法之前 Thread2 and Thread3 invoke method5() before Thread1 invoke method2()

此时 Thread2.p =q,Thread3.p=q, 因为p==t成立 时序图如下,然后Thread1执行方法2 p==t 不执行tail尾节点的更新操作 由此可知 尾节点是延迟更新 一切为了更高效~~~

                              图1                  

Thread 2 与 Thread3 此时再次执行 1 方法 见图1 他们此时的q.next==null 我们规定Thread2 CAS成功 Thread3失败了 成功后的时序图如下 我们假设 Thread3 invoke method5() after Thread2 invoke method2() Thread2执行方法2 在 Thread3执行方法5之前

                 
                                 图2                       

对于Thread2 进入2方法 p!=t 满足 执行 casTail(t, newNode) 更新尾节点的快照 如下图

                                 图3
                                 

Thread2 工作完成 退出循环

对于Thread3 因为执行1方法失败 进入5方法 此时Thread3的tail快照t3

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

按图3来翻译

p=(p!=t3&&t3!=(t3=t2))?t2:q;

p=t2;//直接去当前能获取到的尾节点!!!

到这里 offer() 方法解决完成

ConcurrentLinkedQueue核心总结

tail和head都是 延迟更新的 但是tail更新在head更新后面 因为方法4中 需要依赖head节点 去找每一个存活的节点

前面的叙述中 可以看到 offer() 方法内 核心操作 就是 p=condition?result1:result2

偶数次offer() 操作更新一次tail 单线程的环境下

与Michael-Scott 队列比较

Michael-Scott队列 每次操作 都需要判断是否需要推动尾节点 采取CAS的操作 优点也是缺点

Doug Lead老神仙的CAS 我这个菜鸟猜测 能不用CAS 就尽量不用 因为CAS存在竞争 提供以最少次数的更新达到最终正确的效果

我们把offer()中的整个行为想象为跳台阶 result1的形式就像是 武侠小说中的越阶战斗!!!result2的形式就是一步一个脚印 每次平稳地去下一个台阶

我们想象一下 offer()最优的情况 10个线程同时offer()

每一个执行1方法成功的线程都没有(执行2方法或则执行3方法失败) 没关系 尾节点的更新终会成功

每一个失败的线程都是去当前节点的next节点 p.next进行插入操作 在第9个线程(相当于我们上文中的线程2)

当第10个线程操作时 虽然它很可怜 一直排到最后 但是尾节点更新一下就越过了9阶!!!(不太恰当的地方请大佬们指点) 

ConcurrrntLinkedQueue 优点

能跃过一整段因为多线程在极短时间内offer()插入的节点 直接去尾节点 直接跨过去

能抵达每一个相对于当前快照来说最新的next节点

高并发时 tail 和 p 相互配合 尽力去离当前尾节点 最近的地方

ConcurrentLinkedQueue 缺点

CAS操作 虽然总会成功 但是竞争效率如果很低 不如用同步锁 采用CAS编写并发代码 都是大佬级别 难度高 不接地气(嘿嘿)

循环可能会带来额外的资源开销

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/73225.html

相关文章

  • &lt;java并发编程实战&gt;学习四

    摘要:对象的组合介绍一些组合模式,这些模式能够使一个类更容易成为线程安全的,并且维护这些类时不会无意破坏类的安全性保证。状态变量的所有者将决定采用何种加锁协议来维持变量状态的完整性。所有权意味着控制权。 对象的组合 介绍一些组合模式,这些模式能够使一个类更容易成为线程安全的,并且维护这些类时不会无意破坏类的安全性保证。 设计线程安全的类 在设计线程安全类的过程中,需要包含以下三个基本要素: ...

    tainzhi 评论0 收藏0
  • &lt;java并发编程实战&gt;学习三

    摘要:线程封闭当访问共享的可变数据时,通常需要使用同步。如果仅在单线程内访问数据,就不要同步。这种技术成为线程封闭。栈封闭栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。,对象是正确创建的。 线程封闭 当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不要同步。这种技术成为线程封闭(Thread Confine...

    Richard_Gao 评论0 收藏0
  • &lt;java并发编程实战&gt;学习一

    摘要:无状态的是线程安全的,当无状态变为有状态时就是不安全的破坏了线程的安全性,非原子性操作竞态条件在并发编程中,由于不恰当的执行时序而出现的不正确结果是一种非常重要的情况,被称之为竞态条件。重入意味着获取锁的操作的粒度是线程,而不是调用。 这本书的内容是什么? 本书提供了各种实用的设计规则,用于帮助开发人员创建安全的和高性能的并发类。 什么类是线程安全的? 当多个线程访问某...

    xiaoqibTn 评论0 收藏0
  • &lt;&lt;深入PHP面向对象、模式与实践&gt;&gt;读书笔记:面向对象设计和过程式编程

    摘要:注本文内容来深入面向对象模式与实践中节。面向对象设计与过程式编程面向对象设计和过程式编程有什么不同呢可能有些人认为最大的不同在于面向对象编程中包含对象。面向对象编程和过程式编程的一个核心区别是如何分配职责。 注:本文内容来中6.2节。 6.2 面向对象设计与过程式编程   面向对象设计和过程式编程有什么不同呢?可能有些人认为最大的不同在于面向对象编程中包含对象。事实上,这种说法不准确。...

    xiao7cn 评论0 收藏0
  • JavaScript设计模式与开发实践》 —— &lt;阅读小札·一&gt;

    摘要:阅读小札一阅读前自大学课上,就开始接触设计模式,但对设计模式却鲜有研究与实践。第二部分是核心部分,由浅到深讲解个设计模式。设计模式遵循的原则所有设计模式罪训的一条原则就是找出程序中变化的地方,并将变化封装起来。 阅读小札 · 阅读前 自大学Java课上,就开始接触设计模式,但对设计模式却鲜有研究与实践。最近向公司反映和游说技术提升,得以获得公司提供购书机会,借此认真学习前端学习之路的...

    Yangder 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<