聊聊并发(六)ConcurrentLinkedQueue的实现原理分析

本文是作者原创,首发于InfoQ:http://www.infoq.com/cn/articles/ConcurrentLinkedQueue

1.    引言

在并发编程中我们有时候需要使用线程安全的队列。如果我们要实现一个线程安全的队列有两种实现方式一种是使用阻塞算法,另一种是使用非阻塞算法。使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现,而非阻塞的实现方式则可以使用循环CAS的方式来实现,本文让我们一起来研究下Doug Lea是如何使用非阻塞的方式来实现线程安全队列ConcurrentLinkedQueue的,相信从大师身上我们能学到不少并发编程的技巧。

2.    ConcurrentLinkedQueue的介绍

ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。它采用了“wait-free”算法来实现,该算法在Michael & Scott算法上进行了一些修改, Michael & Scott算法的详细信息可以参见参考资料一

3.    ConcurrentLinkedQueue的结构

我们通过ConcurrentLinkedQueue的类图来分析一下它的结构。

(图1)

     ConcurrentLinkedQueue由head节点和tair节点组成,每个节点(Node)由节点元素(item)和指向下一个节点的引用(next)组成,节点与节点之间就是通过这个next关联起来,从而组成一张链表结构的队列。默认情况下head节点存储的元素为空,tair节点等于head节点。

查看源代码

打印帮助

1 private transient volatile Node<e> tail = head;

4.    入队列

入队列就是将入队节点添加到队列的尾部。为了方便理解入队时队列的变化,以及head节点和tair节点的变化,每添加一个节点我就做了一个队列的快照图。

(图二)

  • 第一步添加元素1。队列更新head节点的next节点为元素1节点。又因为tail节点默认情况下等于head节点,所以它们的next节点都指向元素1节点。
  • 第二步添加元素2。队列首先设置元素1节点的next节点为元素2节点,然后更新tail节点指向元素2节点。
  • 第三步添加元素3,设置tail节点的next节点为元素3节点。
  • 第四步添加元素4,设置元素3的next节点为元素4节点,然后将tail节点指向元素4节点。

通过debug入队过程并观察head节点和tail节点的变化,发现入队主要做两件事情,第一是将入队节点设置成当前队列尾节点的下一个节点。第二是更新tail节点,如果tail节点的next节点不为空,则将入队节点设置成tail节点,如果tail节点的next节点为空,则将入队节点设置成tail的next节点,所以tail节点不总是尾节点,理解这一点对于我们研究源码会非常有帮助。

上面的分析让我们从单线程入队的角度来理解入队过程,但是多个线程同时进行入队情况就变得更加复杂,因为可能会出现其他线程插队的情况。如果有一个线程正在入队,那么它必须先获取尾节点,然后设置尾节点的下一个节点为入队节点,但这时可能有另外一个线程插队了,那么队列的尾节点就会发生变化,这时当前线程要暂停入队操作,然后重新获取尾节点。让我们再通过源码来详细分析下它是如何使用CAS算法来入队的。

查看源代码

打印帮助

01 public boolean offer(E e) {
02  
03         if (e == null) throw new NullPointerException();
04  
05         //入队前,创建一个入队节点
06  
07         Node</e><e> n = new Node</e><e>(e);
08  
09         retry:
10  
11         //死循环,入队不成功反复入队。
12  
13         for (;;) {
14  
15             //创建一个指向tail节点的引用
16  
17             Node</e><e> t = tail;
18  
19             //p用来表示队列的尾节点,默认情况下等于tail节点。
20  
21             Node</e><e> p = t;
22  
23             for (int hops = 0; ; hops++) {
24  
25             //获得p节点的下一个节点。
26  
27                 Node</e><e> next = succ(p);
28  
29      //next节点不为空,说明p不是尾节点,需要更新p后在将它指向next节点
30  
31                 if (next != null) {
32  
33                    //循环了两次及其以上,并且当前节点还是不等于尾节点
34  
35                     if (hops > HOPS && t != tail)
36  
37                         continue retry;
38  
39                     p = next;
40  
41                 }
42  
43                 //如果p是尾节点,则设置p节点的next节点为入队节点。
44  
45                 else if (p.casNext(null, n)) {
46  
47                   //如果tail节点有大于等于1个next节点,则将入队节点设置成tair节点,更新失败了也没关系,因为失败了表示有其他线程成功更新了tair节点。
48  
49 if (hops >= HOPS)
50  
51                         casTail(t, n); // 更新tail节点,允许失败
52  
53                     return true;
54  
55                 }
56  
57                // p有next节点,表示p的next节点是尾节点,则重新设置p节点
58  
59                 else {
60  
61                     p = succ(p);
62  
63                 }
64  
65             }
66  
67         }
68  
69     }

从源代码角度来看整个入队过程主要做二件事情。第一是定位出尾节点,第二是使用CAS算法能将入队节点设置成尾节点的next节点,如不成功则重试。

第一步定位尾节点。tail节点并不总是尾节点,所以每次入队都必须先通过tail节点来找到尾节点,尾节点可能就是tail节点,也可能是tail节点的next节点。代码中循环体中的第一个if就是判断tail是否有next节点,有则表示next节点可能是尾节点。获取tail节点的next节点需要注意的是p节点等于p的next节点的情况,只有一种可能就是p节点和p的next节点都等于空,表示这个队列刚初始化,正准备添加第一次节点,所以需要返回head节点。获取p节点的next节点代码如下

查看源代码

打印帮助

1 final Node</e><e> succ(Node</e><e> p) {
2  
3          Node</e><e> next = p.getNext();
4  
5          return (p == next) ? head : next;
6  
7      }

第二步设置入队节点为尾节点。p.casNext(null, n)方法用于将入队节点设置为当前队列尾节点的next节点,p如果是null表示p是当前队列的尾节点,如果不为null表示有其他线程更新了尾节点,则需要重新获取当前队列的尾节点。

hops的设计意图。上面分析过对于先进先出的队列入队所要做的事情就是将入队节点设置成尾节点,doug lea写的代码和逻辑还是稍微有点复杂。那么我用以下方式来实现行不行?

查看源代码

打印帮助

01 public boolean offer(E e) {
02  
03        if (e == null)
04  
05          throw new NullPointerException();
06  
07       Node</e><e> n = new Node</e><e>(e);
08  
09       for (;;) {
10  
11          Node</e><e> t = tail;
12  
13          if (t.casNext(null, n) && casTail(t, n)) {
14  
15             return true;
16  
17          }
18  
19       }
20  
21     }

让tail节点永远作为队列的尾节点,这样实现代码量非常少,而且逻辑非常清楚和易懂。但是这么做有个缺点就是每次都需要使用循环CAS更新tail节点。如果能减少CAS更新tail节点的次数,就能提高入队的效率,所以doug lea使用hops变量来控制并减少tail节点的更新频率,并不是每次节点入队后都将 tail节点更新成尾节点,而是当 tail节点和尾节点的距离大于等于常量HOPS的值(默认等于1)时才更新tail节点,tail和尾节点的距离越长使用CAS更新tail节点的次数就会越少,但是距离越长带来的负面效果就是每次入队时定位尾节点的时间就越长,因为循环体需要多循环一次来定位出尾节点,但是这样仍然能提高入队的效率,因为从本质上来看它通过增加对volatile变量的读操作来减少了对volatile变量的写操作,而对volatile变量的写操作开销要远远大于读操作,所以入队效率会有所提升。

查看源代码

打印帮助

1 private static final int HOPS = 1;

还有一点需要注意的是入队方法永远返回true,所以不要通过返回值判断入队是否成功。

5.    出队列

    出队列的就是从队列里返回一个节点元素,并清空该节点对元素的引用。让我们通过每个节点出队的快照来观察下head节点的变化。

从上图可知,并不是每次出队时都更新head节点,当head节点里有元素时,直接弹出head节点里的元素,而不会更新head节点。只有当head节点里没有元素时,出队操作才会更新head节点。这种做法也是通过hops变量来减少使用CAS更新head节点的消耗,从而提高出队效率。让我们再通过源码来深入分析下出队过程。

查看源代码

打印帮助

01 public E poll() {
02  
03            Node</e><e> h = head;
04  
05        // p表示头节点,需要出队的节点
06  
07            Node</e><e> p = h;
08  
09            for (int hops = 0;; hops++) {
10  
11                 // 获取p节点的元素
12  
13                 E item = p.getItem();
14  
15                 // 如果p节点的元素不为空,使用CAS设置p节点引用的元素为null,如果成功则返回p节点的元素。
16  
17                 if (item != null && p.casItem(item, null)) {
18  
19                      if (hops >= HOPS) {
20  
21                           //将p节点下一个节点设置成head节点
22  
23                           Node</e><e> q = p.getNext();
24  
25                           updateHead(h, (q != null) ? q : p);
26  
27                      }
28  
29                      return item;
30  
31                 }
32  
33                 // 如果头节点的元素为空或头节点发生了变化,这说明头节点已经被另外一个线程修改了。那么获取p节点的下一个节点
34  
35                 Node</e><e> next = succ(p);
36  
37                 // 如果p的下一个节点也为空,说明这个队列已经空了
38  
39                 if (next == null) {
40  
41               // 更新头节点。
42  
43                      updateHead(h, p);
44  
45                      break;
46  
47                 }
48  
49                 // 如果下一个元素不为空,则将头节点的下一个节点设置成头节点
50  
51                 p = next;
52  
53            }
54  
55            return null;
56  
57      }

首先获取头节点的元素,然后判断头节点元素是否为空,如果为空,表示另外一个线程已经进行了一次出队操作将该节点的元素取走,如果不为空,则使用CAS的方式将头节点的引用设置成null,如果CAS成功,则直接返回头节点的元素,如果不成功,表示另外一个线程已经进行了一次出队操作更新了head节点,导致元素发生了变化,需要重新获取头节点。

时间: 2024-10-27 20:12:09

聊聊并发(六)ConcurrentLinkedQueue的实现原理分析的相关文章

色彩搭配原理分析及技巧

色彩搭配原理分析及技巧: 研究色彩是为了使用色彩,也就是说最大限度地发挥色彩的作用.色彩的意义与内容在艺术创造和表现方面是复杂多变的,但在欣赏和解释方面又有共通的国际特性,可见它在人们心目中不但是活的,也是一种很美的大众语言.所以,通过对色彩的各种心理分析,找出它们的各种特性,可以做到合理而有效地使用色彩. 一.红色系 红色系是从色相环上的红紫色到朱红色之间的色彩.它的刺激作用很大,具有很高的注目性和视认性.大红色是暖色系里温度最高的色彩,红色系原色彩对人的心理能产生很大的鼓舞作用. 1.纯色使

AbstractQueuedSynchronizer的介绍和原理分析(转)

  简介 提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架.该同步器(以下简称同步器)利用了一个int来表示状态,期望它能够成为实现大部分同步需求的基础.使用的方法是继承,子类通过继承同步器并需要实现它的方法来管理其状态,管理的方式就是通过类似acquire和release的方式来操纵状态.然而多线程环境中对状态的操纵必须确保原子性,因此子类对于状态的把握,需要使用这个同步器提供的以下三个方法对状态进行操作: java.util.concurrent.locks.Abst

Clojure的并发(二)Write Skew分析

Clojure 的并发(一) Ref和STM Clojure 的并发(二)Write Skew分析Clojure 的并发(三)Atom.缓存和性能 Clojure 的并发(四)Agent深入分析和Actor Clojure 的并发(五)binding和let Clojure的并发(六)Agent可以改进的地方Clojure的并发(七)pmap.pvalues和pcallsClojure的并发(八)future.promise和线程      在介绍Ref的上一篇blog提到,基于snapshot

ASP组件上传的三种机制和实现原理分析

上传 ASP 组件 FILE对象 当前,基于浏览器/服务器模式的应用比较流行.当用户需要将文件传输到服务器上时,常用方法之一是运行FTP服务器并将每个用户的FTP默认目录设为用户的Web主目录,这样用户就能运行FTP客户程序并上传文件到指定的 Web目录.这就要求用户必须懂得如何使用FTP客户程序.因此,这种解决方案仅对熟悉FTP且富有经验的用户来说是可行的. 如果我们能把文件上传功能与Web集成,使用户仅用Web浏览器就能完成上传任务,这对于他们来说将是非常方便的.但是,一直以来,由于File

搜索引擎判断网站是否作弊的原理分析(三)

广州SEO陈永继续为大家讲解搜索引擎判断网站如何判断网站是否作弊的原理,上节讲解完TrustRank算法,这一节将详细讲解BadRank算法. BadRank据传是Google采用的反链接作弊算法.它是一种典型的不信任传播模型,即首先构建作弊网页集合,之后利用链接关系来讲这种不信任分值传递到其他网页. BadRank包含的基本假设是:如果一个网页将其链接指向作弊页面,则这个网页也很可能是作弊网页:而如果一个网页被作弊网页指向,则不能说明这个网页是有问题的,因为作弊网页也经常将其链接指向一些知名网

搜索引擎判断网站是否作弊的原理分析(二)

承接搜索引擎判断网站是否作弊的原理分析(一) 广州SEO陈永继续为大家分析信任传播模型.不信任传播模型及异常发现模型3个代表算法,它们分别是TrustRank算法.BadRank算法和SpamRank算法. 我们先详细介绍TrustRank算法 TrustRank算法属于信任传播模型,基本遵循信任传播模型的流程,即算法流程如下两个步骤组成. 步骤一:确定值得信任的网页集合 TrustRank算法需要靠人工审核来判断某个网页应该被放入网页集合,考虑到人工审核工作量大,所以提出了两种初选信任网页集合

IOS开发:Cocos2d触摸分发原理分析

  触摸是iOS程序的精髓所在,良好的触摸体验能让iOS程序得到非常好的效果,例如Clear.鉴于同学们只会用cocos2d的 CCTouchDispatcher 的 api 但并不知道工作原理,但了解触摸分发的过程是极为重要的.毕竟涉及到权限.两套协议等的各种分发. 本文以cocos2d-iphone源代码为讲解.cocos2d-x 于此类似,就不过多赘述了. 零.cocoaTouch的触摸 在讲解cocos2d触摸协议之前,我觉得我有必要提一下CocoaTouch那四个方法.毕竟cocos2

Photoshop图层与色彩原理分析

  PS入门教程 Photoshop图层与色彩原理分析   教程结束,以上就是Photoshop图层与色彩原理分析,希望大家看完这篇教程之后能有一定的帮助! 分类: PS入门教程

web上存漏洞及原理分析、防范方法(文件名检测漏洞)

我们通过前篇:<web上存漏洞及原理分析.防范方法(安全文件上存方法) >,已经知道后端获取服务器变量,很多来自客户端传入的.跟普通的get,post没有什么不同.下面我们看看,常见出现漏洞代码.1.检测文件类型,并且用用户上存文件名保存 复制代码 代码如下: if(isset($_FILES['img'])) { $file = save_file($_FILES['img']); if($file===false) exit('上存失败!'); echo "上存成功!"