java并发之ConcurrentLinkedQueue

在并发编程中,我们可能经常需要用到线程安全的队列,java为此提供了两种模式的队列:阻塞队列和非阻塞队列。

:阻塞队列和非阻塞队列如何实现线程安全?

  • 阻塞队列可以用一个锁(入队和出队共享一把锁)或者两个锁(入队使用一把锁,出队使用一把锁)来实现线程安全,JDK中典型的实现是BlockingQueue
  • 非阻塞队列可以用循环CAS的方式来保证数据的一致性,来达到线程安全的目的。

接下来我们就来看看JDK是如何使用非阻塞的方式来实现线程安全队列ConcurrentLinkedQueue的。

ConcurrentLinkedQueue源码分析

ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,遵循队列的FIFO原则,队尾入队,队首出队。我们先来看下队列的基础数据结构以及初始化相关源码实现。

队列基础数据结构Node及初始化

  • Node实现

    Node实现

    从源码可以看出,Node有两个私有属性item和指向下一个节点的next,为了降低开销,item和next都被声明成volatile类型,同时,它们使用CAS来保证更新的线程安全。

  • ConcurrentLinkedQueue数据结构

    ConcurrentLinkedQueue私有属性

    ConcurrentLinkedQueue由head和tail节点组成,节点与节点之间通过next连接,从而来组成一个链表结构的队列。

  • 队列初始化
    ConcurrentLinkedQueue有两个私有属性,head和tail:

    ConcurrentLinkedQueue私有属性

    接下来我们来看看ConcurrentLinkedQueue是如何初始化的。
    ConcurrentLinkedQueue初始化

    从源码可以看出,当初始化一个ConcurrentLinkedQueue对象时,会创建一个item和next都为null的节点,并让head和tail都指向该节点,初始化后的队列结构如下:
    初始化后结构图

看完数据结构及初始化后,接下里我们就该来看看队列的两个重要实现:入队和出队。

ConcurrentLinkedQueue入队

offer实现

入队的实质就是在队尾做节点插入,具体的执行流程如下:

  1. 调用checkNotNull方法判断待入队元素是否为null,如果为null则抛出空指针异常;

  2. 创建一个待入队节点;

  3. 循环执行队尾插入:

    • 情况1:如果tail节点的下一个节点q为null,通过p.casNext(null, newNode)将p节点的next节点设置为待入队节点:

      • CAS设置成功:比较p和t,如果p不等t,将tail节点设置为待入队节点,入队成功,返回true,如果p等于t,直接返回true;
      • CAS设置不成功,表明有其他的线程对tail节点有所更改,那么,继续执行for循环,直到入队成功。
    • 情况3:变换一下顺序,先说最后一种情况,改写一下代码,就比较容易理解了:


      更改后代码

      可以看出来,如果p和t相等,则将p指向q,否则,判断tail节点是否发生变化,如果没有发生变化,将p指向q,如果发生变化,设置p为尾节点;

    • 情况2:通过情况3的操作,p和q相等的情况就可能会出现了,此时,若tail节点没有发生变化,那么应该就是head节点发生了变化,设置p为head节点,从头开始遍历队列,如果是tail节点发生变化,设置p为tail节点。

到这里为止,入队的源码分析差不多,说实在的,还是很懵,大家心里可能可能还在纠结,第一,入队的过程到底是什么样子的呀?第二,入队直接CAS更新tail节点就可以了,为什么还要那么费劲的分情况处理?

针对第一个问题,给出一个图,大家就能完全明白了:

队列入队结构变化图.jpg

  1. 添加节点1,此时tail节点的next节点为null,符合上述代码中的情况1,更新队列的tail节点的next节点为元素1,由于初始化是head节点等于tail节点,所以此时head和tail的next节点均指向节点1;

  2. 添加节点2,此时tail节点的next节点不为null,同时p也不等于q,符合上述代码中的情况3,首先执行情况3将p指向tail节点的next节点,再执行情况1相关逻辑,设置节点1的next节点为元素2,此时p不等于t,更新tail节点,将tail节点指向元素2;

节点3和4入队过程与1和2入队一致,在这里我就不再做赘述。需要注意的是:tail节点并不一定是指向队列的最后一个节点,它可能指向最后一个节点的前一个节点!!!

针对第二个问题,我们来尝试换一种方式思考,假如我们每次就让tail节点作为队尾节点,每次的入队所要做的事情其实就是将入队节点设置成尾节点,代码可以简化成这样:

tail为队尾节点的入队

上述代码量确实非常少,而且逻辑非常清楚和易懂,但是这样做有个缺点就是每次入队都需要循环CAS更新tail节点。
如果能减少更新tail节点的次数,那么就能提高入队的效率,所以,Doug Lea并没有让tail节点作为队尾节点,只有tail节点与队尾节点之间的距离等于1的时候才需要更新tail节点。但是,这样就可能导致当队列长度越长的时候每次入队定位尾节点的时间就会越长,即便是这样,它仍然可以提高入队效率,因为从本质上来看,volatile变量的写操作的开销要远远大于读操作的。

分析完入队,接下来我们来看看ConcurrentLinkedQueue的出队。

ConcurrentLinkedQueue出队

poll实现

出队的实质就是情况表头节点的引用并返回表头节点的值,具体的之行逻辑如下:

  1. 获取头结点的元素;

  2. 如果表头节点的元素不为null,并且调用p.casItem(item, null)设置表头节点数据为null成功:

    • 如果p不等于head节点,此时表头发生了变化,调用updateHead方法更新表头节点,然后返回删除节点item;
    • 否则,不更新表头节点,直接返回删除节点item。
  3. 如果步骤2条件不成立并且表头节点的next节点q为null,那么此时队列只有一个为null的节点,调用updateHead方法更新表头节点为p,然后返回null;

  4. 如果步骤2和3的条件均不成立并且p等于q,跳转到restartFromHead标记重新执行;

  5. 步骤2,3,4均不成立,将p指向q;

  6. 循环执行上述步骤;

源码其实就这么多,为了方便理解出队的过程,还是照样给一个图:


队列出队结构变化图
  1. 节点1出队,此时head的item为null,执行上述代码逻辑中的步骤5,p指向节点1,此时p的item域不为空,执行步骤2,将节点1的item设置为null,此时p不等于h,更新头结点(如果p的next节点不为null,将头结点指向q,否则指向p),返回节点1的item,head执行节点2;

  2. 节点2出队,此时head的item不为null,执行上述代码逻辑中步骤2,将节点2的item设置为null,此时p等于h,直接返回节点2的item,head仍然指向节点2;

节点3和4出队过程与1和2出队一致,在这里我就不再做赘述。需要注意的是head节点并不一定是指向队列的第一个有效节点,它可能指向有效节点的前一个节点!!!

注:这里的有效节点是指从head节点向后遍历可达的节点当中,item不为null的节点。

当然,为什么head节点不总是指向队列的第一个有效节点,其原因跟入队是一样的,这么做的最主要也是减少CAS更新head节点的次数,从而提高出队效率。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容