1.简介

ConcurrentLinkedQueue是JUC中的基于链表的无锁队列实现。本文将解读其源码实现。

2. 论文

ConcurrentLinkedQueue的实现是以Maged M. Michael和Michael L. Scott的论文Simple, Fast, and Practical Non-Blocking and Blocking Concurrent Queue Algorithms为原型进行改造的,不妨阅读此篇论文。

下面我将论文中的介绍的入队与出队用接近Java语言的形式改写并加上注释。

enq
node = new Node(value, null)
loop
tail = this.tail
next = tail.next
# 如果tail已经不是尾节点,重试循环。
if tail == this.tail
# 队列处于稳定状态,尝试插入节点。
if next == null
# 插入新节点,将尾节点与新节点链接起来。
# 如果成功则退出循环,否则重试。
if CAS(tail.next, next, <node, next.count+1>
break
# 队列处于中间状态,推进尾节点。
else
CAS (this.tail, tail, <next, tail.count+1>)
# 将尾节点更新为新插入的节点,失败没关系,说明其它线程更新了尾节点。
CAS(this.tail, tail, <node, tail.count+1>) deq
loop
head = this.head
tail = this.tail
next = head.next
# 如果head已经不是头节点,重试循环。
if head == this.head
if head == tail
# 队列处于稳定状态则出队失败。
if next == null
return false
# 有其它线程正在入队,推进尾节点。
CAS(this.tail, tail, <next, tail.count+1>)
else
# 成功将队列头节点CAS到下一个节点则出队成功,退出循环。
if CAS(this.head, head, <next, head.count+1>)
break
return true

由于Java自带垃圾回收,加上ConcurrentLinkedQueue对节点进行CAS且其内外方法都保证了节点不会复用,所以并不会出现ABA问题,因此节点不需要版本号。

3. ConcurrentLinkedQueue的实现

3.1 数据结构

正如典型的队列设计,内部的节点用如下的Node类表示

/**
* 仅展示属性,其余略去。
*/
private static class Node<E> {
volatile E item;
volatile Node<E> next;
}

值得一提的是Node中有一个lazySetNext方法

void lazySetNext(Node<E> val) {
UNSAFE.putOrderedObject(this, nextOffset, val);
}

与AtomicReference类一样,使用了UNSAFE.putOrderedObject方法来实现低延迟的写入。这个方法会插入Store-Store内存屏障,也就是保证写操作不会重排。而不会插入普通volatile写会插入的Store-Load屏障。

ConcurrentLinkedQueue在构造时会初始化head和tail为一个item为null的节点,作为哨兵节点。

private transient volatile Node<E> head;

private transient volatile Node<E> tail;

3.2 设计思想

ConcurrentLinkedQueue的源码还是有些晦涩难懂的,但是doc非常详细,对阅读源码非常有帮助。如果带着从doc中介绍的设计与实现思路去读源码会轻松不少。

ConcurrentLinkedQueue是不允许向其插入空的item的,对于删除元素,会将其item给CAS为null,一旦某个元素的item变为null,就意味着它不再是队列中的有效元素了,并且会将已删除节点的next指针指向自身。

这样可以实现尽可能快地从已删除的元素跳过后面删除的元素,回到队列中。

ConcurrentLinkedQueue具有以下这些性质:

  • 队列中任意时刻只有最后一个元素的next为null
  • head和tail不会是null(哨兵节点的设计)
  • head未必是队列中第一个元素(head指向的可能是一个已经被移除的元素)
  • 队列中的有效元素都可以从head通过succ方法遍历到
  • tail未必是队列中最后一个元素(tail.next可以不为null)
  • 队列中的最后一个元素可以从tail通过succ方法遍历到
  • tail甚至可以是head的前驱

这里提到了succ方法,那么先睹为快,看一下succ方法吧。

final Node<E> succ(Node<E> p) {
Node<E> next = p.next;
// 如果next就是自身(代表已经不在队列中),则返回head,否则返回next。
return (p == next) ? head : next;
}

因为ConcurrentLinkedQueue中的head和tail都可能会滞后,这其实是一种避免频繁CAS的优化。当然过度的滞后也是会影响操作效率的,所以在具体实现的时候,会尽可能能有机会更新head和tail就去更新它们。

3.3 源码解读

3.3.1 offer方法

public boolean offer(E e) {
checkNotNull(e);
final Node<E> newNode = new Node<E>(e); for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
// 如果p的next为null,则说明此刻p为队列中最后一个元素。
if (q == null) {
/*
* cas成功则newNode成功入队,只是此刻tail还是老的。
* 否则说明因为线程竞争的关系没有成功入队,需要重试。
*/
if (p.casNext(null, newNode)) {
/*
* t是当前线程读到的tail快照,p是上面CAS时队列中最后一个元素。
* 这两者不一致说明该更新tail了。
* 如果CAS失败则说明tail已经被其它线程更新过了,这没关系。
*/
if (p != t)
casTail(t, newNode);
return true;
}
}
/*
* ConcurrentLinkedQueue的一个设计就是对于已经移除的元素,
* 会将next置为本身,用于判断当前元素已经出队,接着从head继续遍历(可以看succ方法)。
*
* 在整个offer方法的执行过程中,p一定是等于t或者在t的后面的,
* 因此如果p已经不在队列中的话,t也一定不在队列中了。
*
* 所以重新读取一次tail到快照t,
* 如果t未发生变化,就从head开始继续下去。
* 否则让p从新的t开始继续尝试入队是一个更好的选择(此时新的t很可能在head后面)
*/
else if (p == q)
p = (t != (t = tail)) ? t : head;
else
/*
* 如果p与t相等,则让p继续向后移动一个节点。
*
* 如果p和t不相等,则说明已经经历至少两轮循环(仍然没有入队),
* 则重新读取一次tail到t,如果t发生了变化,则从t开始再次尝试入队。
*/
p = (p != t && t != (t = tail)) ? t : q;
}
}

3.3.2 poll方法

public E poll() {
restartFromHead:
for (;;) {
// p初始设置为head。
for (Node<E> h = head, p = h, q;;) {
E item = p.item; /*
* 成功将item给CAS为null则说明成功移除了元素。
* 这里的item != null判断也是为了尽可能避免无意义的CAS。
*/
if (item != null && p.casItem(item, null)) {
/*
* p如果与h不相等,则说明head很可能滞后,指向已不在队列中的元素。
* 如果此时p有后继,则更新head为p.next,
* 否则尽管p已经被移除出去了,也只能更新head为p了。
*/
if (p != h)
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
/*
* 如果没能成功移除p,且p也没有后继,则说明p为此时队列的最后元素。
* 所以更新head为p并返回null。
*
* 注意这里h和p是可能相等的,updateHead会判断h和p是否相等以避免无意义CAS。
*/
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
/*
* p存在后继,需要检查是否p还在队列中。
* 如果p已经不在队列中(p==q),则重新读一次head到快照h并让p从h开始再尝试移除元素。
*
* 因为一定有其它线程已经通过updateHead将head从p给CAS为新的head并且令p节点的next指向p自己,
* 这时再一步步往后面走显然不值得,不如从现在的head开始重新来过。
*/
else if (p == q)
continue restartFromHead;
// 继续向后走一个节点尝试移除元素。
else
p = q;
}
}
} final void updateHead(Node<E> h, Node<E> p) {
if (h != p && casHead(h, p))
h.lazySetNext(h);
}

3.3.3 peek方法

public E peek() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
// 其实这里的if就是将poll中的if前两个分支做了个合并。
if (item != null || (q = p.next) == null) {
updateHead(h, p);
return item;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}

3.3.4 remove方法

public boolean remove(Object o) {
if (o != null) {
Node<E> next, pred = null;
// p为当前节点,pred为p前驱,next为后继。
for (Node<E> p = first(); p != null; pred = p, p = next) {
boolean removed = false;
E item = p.item;
// item为null代表元素已经无效(认为不在队列中)
if (item != null) {
// 不是要删除的元素。
if (!o.equals(item)) {
next = succ(p);
continue;
}
removed = p.casItem(item, null);
} next = succ(p);
if (pred != null && next != null)
// 前驱与后继连上。
pred.casNext(p, next);
if (removed)
return true;
}
}
return false;
}

3.3.5 size方法

/**
* size方法效率其实挺差的,是一个O(n)的遍历。
*/
public int size() {
int count = 0;
for (Node<E> p = first(); p != null; p = succ(p))
if (p.item != null)
// 最多只返回Integer.MAX_VALUE
if (++count == Integer.MAX_VALUE)
break;
return count;
} /**
* 这个方法和poll/peek方法差不多,只不过返回的是Node而不是元素。
*
* 之所以peek方法没有复用first方法的原因有2点
* 1. 会增加一次volatile读
* 2. 有可能会因为和poll方法的竞争,导致出现非期望的结果。
* 比如first返回的node非null,里面的item也不是null。
* 但是等到poll方法返回从first方法拿到的node的item的时候,item已经被poll方法CAS为null了。
* 那这个问题只能再peek中增加重试,这未免代价太高了。
*
* 这就是first和peek代码没有复用的原因。
*/
Node<E> first() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
boolean hasItem = (p.item != null);
if (hasItem || (q = p.next) == null) {
updateHead(h, p);
return hasItem ? p : null;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}

ConcurrentLinkedQueue源码解读的更多相关文章

  1. ConcurrentLinkedQueue 源码解读

    一.介绍 ConcurrentLinkedQueue 是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部:当我们获取一个元素时,它 ...

  2. JDK容器类List,Set,Queue源码解读

    List,Set,Queue都是继承Collection接口的单列集合接口.List常用的实现主要有ArrayList,LinkedList,List中的数据是有序可重复的.Set常用的实现主要是Ha ...

  3. SDWebImage源码解读之SDWebImageDownloaderOperation

    第七篇 前言 本篇文章主要讲解下载操作的相关知识,SDWebImageDownloaderOperation的主要任务是把一张图片从服务器下载到内存中.下载数据并不难,如何对下载这一系列的任务进行设计 ...

  4. SDWebImage源码解读 之 NSData+ImageContentType

    第一篇 前言 从今天开始,我将开启一段源码解读的旅途了.在这里先暂时不透露具体解读的源码到底是哪些?因为也可能随着解读的进行会更改计划.但能够肯定的是,这一系列之中肯定会有Swift版本的代码. 说说 ...

  5. SDWebImage源码解读 之 UIImage+GIF

    第二篇 前言 本篇是和GIF相关的一个UIImage的分类.主要提供了三个方法: + (UIImage *)sd_animatedGIFNamed:(NSString *)name ----- 根据名 ...

  6. SDWebImage源码解读 之 SDWebImageCompat

    第三篇 前言 本篇主要解读SDWebImage的配置文件.正如compat的定义,该配置文件主要是兼容Apple的其他设备.也许我们真实的开发平台只有一个,但考虑各个平台的兼容性,对于框架有着很重要的 ...

  7. SDWebImage源码解读_之SDWebImageDecoder

    第四篇 前言 首先,我们要弄明白一个问题? 为什么要对UIImage进行解码呢?难道不能直接使用吗? 其实不解码也是可以使用的,假如说我们通过imageNamed:来加载image,系统默认会在主线程 ...

  8. SDWebImage源码解读之SDWebImageCache(上)

    第五篇 前言 本篇主要讲解图片缓存类的知识,虽然只涉及了图片方面的缓存的设计,但思想同样适用于别的方面的设计.在架构上来说,缓存算是存储设计的一部分.我们把各种不同的存储内容按照功能进行切割后,图片缓 ...

  9. SDWebImage源码解读之SDWebImageCache(下)

    第六篇 前言 我们在SDWebImageCache(上)中了解了这个缓存类大概的功能是什么?那么接下来就要看看这些功能是如何实现的? 再次强调,不管是图片的缓存还是其他各种不同形式的缓存,在原理上都极 ...

随机推荐

  1. Kafka文件存储机制那些事

    Kafka是什么 Kafka是最初由Linkedin公司开发,是一个分布式.分区的.多副本的.多订阅者,基于zookeeper协调的分布式日志系统(也可以当做MQ系统),常见可以用于web/nginx ...

  2. RockChip RK3326 系统编译问题总结

    1. 序言 本文主要记录了RK3326平台系统编译过程中遇到的各种问题,并加以解决! 环境: 宿主Linux:Ubuntu 16.04 目标机:RK3326 (64bit) Toolchain:gcc ...

  3. Ubuntu16.04安装后开发环境配置和常用软件安装

    Ubuntu16.04安装后1.安装常用软件搜狗输入法+编辑器Atom+浏览器Chome+视频播放器vlc+图像编辑器GIMP Image Editor安装+视频录制软件RcordMyDesktop安 ...

  4. .net MVC使用Session验证用户登录(转载)

    .net MVC使用Session验证用户登录   用最简单的Session方式记录用户登录状态 1.添加DefaultController控制器,重写OnActionExecuting方法,每次访问 ...

  5. 本地计算机上的MySQL服务启动后停止。某些服务在未由其他服务或程序使用时将自动

    重新安装MySQL数据库,由于安装的时候马虎,一路next(事实上,某些地方需要严格的配置,我忘记注意了),导致现在出了很多麻烦. 错误信息: 本地计算机上的MySQL服务启动后停止.某些服务在未由其 ...

  6. Nginx + 阿里云SSL + tomcat 实现https访问代理

    第一步:阿里云申请云盾证书服务 第二步:下载证书 第三步:修改Nginx配置 1. 证书文件214033834890360.pem,包含两段内容,请不要删除任何一段内容. 2. 如果是证书系统创建的C ...

  7. 不固定个数组,进行一一对应的组合,js将多个数组实现排列组合

    var arr = [ ["a", "b"], ["1", "2"], ["d"] ]; var s ...

  8. Docker-compose 编排工具安装

    介绍 Compose 是一个定义和管理多容器的工具,使用Python语言编写,使用Compose配置文件描述多个容器应用的架构, 比如什么镜像,数据卷,网络,映射端口等:然后一条命令管理所有服务,比如 ...

  9. React Native中Mobx的使用

    从今天开始我们来搞搞状态管理可否,这几天没怎么写博客,因为被病魔战胜了,tmd,突然的降温让我不知所措,大家最近注意安全,毕竟年底了,查的严,呸,大家注意保暖 特别声明:写该文只是写一下用MobX的思 ...

  10. 【读书笔记】iOS-“一心多用”利用多线程提升性能

    iPhone将具有支持不同类型多线程API的能力,这些API包括:POSIX线程,NSObject,NSThread和NSOperation. iPhone操作系统是一个真正的抢占式,多任务操作系统, ...