本文开始介绍并发队列,为后面介绍线程池打下基础。并发队列莫非也是出队入队操作,还有一个比较重要的点就是如何保证其线程安全性,有些并发队列保证线程安全是通过lock,有些是通过CAS

我们从ConcurrentLinkedQueue开始吧。

1. 介绍

ConcurrentLinkedQueue集合框架的一员,是一个无界限且线程安全,基于单向链表的队列。该队列的顺序是FIFO。当多线程访问公共集合时,使用这个类是一个不错的选择。不允许null元素。是一个非阻塞的队列。

它的迭代器是弱一致性的,不会抛出java.util.ConcurrentModificationException,也可能在迭代期间,其他操作也正在进行。size()方法,不能保证是正确的,因为在迭代时,其他线程也可以操作该队列。

1.1 类图



(显示的方法都是公有方法)

public class ConcurrentLinkedQueue<E> extends AbstractQueue<E>
implements Queue<E>

继承至AbstractQueue,他提供了队列操作的一个框架,有基本的方法,addremoveelement等等,这些方法基于offerpollpeek(最主要看这几个方法)。

2. 源码分析

2.1 类的整体结构

队列中的元素Node

private static class Node<E> {
// 保证两个字段的可见性
volatile E item;
volatile Node<E> next; /**
* Constructs a new node. Uses relaxed write because item can
* only be seen after publication via casNext.
*/
Node(E item) {
UNSAFE.putObject(this, itemOffset, item);
} boolean casItem(E cmp, E val) {
return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
} void lazySetNext(Node<E> val) {
// putOrderedXXX是putXXXVolatile的延迟版本,设置某个值不会被其他线程立即看到(可见性)
// putOrderedXXX设置的值的修饰应该是volatile,这样该方法才有用 // 关于为什么使用这个方法,主要目的肯定是提高效率,但是具体原理,我只能告诉大家跟内存屏障有关(我也不太清楚这一块,待我研究后,再写一篇文章)
UNSAFE.putOrderedObject(this, nextOffset, val);
} boolean casNext(Node<E> cmp, Node<E> val) {
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
} // Unsafe类中的东西,可以去了解一下 private static final sun.misc.Unsafe UNSAFE;
private static final long itemOffset;
private static final long nextOffset; static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> k = Node.class;
itemOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("item"));
nextOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("next"));
} catch (Exception e) {
throw new Error(e);
}
}
}

构造器1:

    // private transient volatile Node<E> head;
// private transient volatile Node<E> tail;
public ConcurrentLinkedQueue() {
head = tail = new Node<E>(null);
}

构造器2:

public ConcurrentLinkedQueue(Collection<? extends E> c) {
Node<E> h = null, t = null;
for (E e : c) {
checkNotNull(e);
Node<E> newNode = new Node<E>(e);
if (h == null)
h = t = newNode;
else {
t.lazySetNext(newNode);
t = newNode;
}
}
if (h == null)
h = t = new Node<E>(null);
head = h;
tail = t;
}

下面开始讲方法,从offerpollpeek从这几个方法入手

2.2 offer

添加元素到队尾。因为队列是无界的,这个方法永远不会返回false

分为三种情况进行分析(一定自己跟着代码debug,一步步的走)

  1. 单线程时(使用IDEA debug一直进入的是 else if把我搞迷茫了,我会写一个博客来解释原因
        ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
queue.offer("A");
queue.offer("B");

以上面的代码,分析每一个步骤。

执行构造函数后:

此时链表的head与tail指向哨兵节点

插入"A", 此时没有设置tail('两跳机制',这里的原因后面详见)

插入"B",

单线程情况比较简单

  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;
if (q == null) {
// p is last node
// 只有一个线程能够CAS成功,其余的都重试
if (p.casNext(null, newNode)) { // 延迟设置tail,第一个node入队不会设置tail,第二个node入队才会设置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
}
// 这里是有其他线程正在poll操作才会进入,此时只考虑多线程offer的情况,暂不分析
else if (p == q)
// 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
// Check for tail updates after two hops.
// 存在tail被更改前,和更改后的两种情况
p = (p != t && t != (t = tail)) ? t : q;
}
}

结合上面的代码,看图

  • 步骤一线程A线程B都执行到
   if (p.casNext(null, newNode))

  • 步骤二,只有一个线程执行成功,假设线程A成功,线程B失败



    因为p(a) == t(a), 此时不执行casTailtail不变。q = p.next, 所以此时q(b) = Node2 ,那么 p(b) != q(b), 线程B执行p = (p != t && t != (t = tail)) ? t : q;

线程B即将执行

   p = (p != t && t != (t = tail)) ? t : q;
  • 步骤三 此时线程C进入。

    此时,p(c) != q(c), 线程C执行
   p = (p != t && t != (t = tail)) ? t : q;

执行完后,q(c)赋值给p(c). 再次循环,此时,q(c) == null, 设置p(c)的next,线程C将值入队

  • 步骤四 p(c) != t(c), 线程C执行casTail(t, newNode), 线程C设置尾结点

  • 此时线程B执行
   p = (p != t && t != (t = tail)) ? t : q;

因为p(b) == t(b),所以 q(b) 赋值给 p(b)。继续循环,最后得到

  1. 多线程的另一种情况,回到步骤三,此时线程C把值入队了,但是还没有设置tail

  • 线程B,将值入队成功

    步骤三的基础上,线程B入队成功后,目前的状况如下:

此时,线程C执行casTail(t, newNode),但是现在的tail != t(c), CAS失败, 直接返回。

2.2.1 小结

上面不管是多线程还是单线程,都是努力的去寻找next为null的节点,若为next节点为null,再判断是否满足设置tail的条件。

多线程offer的第一种情况存在设置tail滞后的问题,我把它称之为"两跳机制",后面讲使用这种机制的原因。

我们看到上面的情况一直没有进入else if (p == q)分支,进入else if分支只会发生在有其他线程在poll时,我们先讲讲poll,再讲讲何时进入else if分支。

2.3 poll

删除并返回头结点的值

简单提一下单线程多线程poll,着重分析一下polloffer共存的情况

  1. 单线程时



    单线程比较简单,就不画图了,按照上面的queue,进行一步一步的debug就行了

  2. 多线程,只有poll

 public E poll() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item; // casItem这里只有一个线程能够成功,其余的继续下面的代码
if (item != null && p.casItem(item, null)) {
// Successful CAS is the linearization point
// for item to be removed from this queue.
if (p != h) // hop two nodes at a time
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
    final void updateHead(Node<E> h, Node<E> p) {
if (h != p && casHead(h, p))
// 将之前的头节点,自己指向自己,等待被GC
h.lazySetNext(h);
}

从上面代码可以看出,修改itemhead都会使用CAS,这些变量都是被volatile修饰,所以保证了这些变量的线程安全性。不管是单线程还是多线程的poll,它们都是去寻找一个有效的头节点,删除并返回该值,若不是有效的就继续找,若队列为空了,就返回null

最后分析一下,offerpoll共存的情况

  • 线程Aoffer操作,线程Bpoll操作,初始的状态如下:

  • 线程A进入。

  • 线程A将要执行

Node<E> q = p.next;

线程B进入,进行poll操作

此时,线程B执行了一次内循环,将q(b)赋值给了p(b);

  • 线程B再次执行内循环,此时将p(b).item置空,将p(b)赋值给head,之前的h(b)next指向自己,线程B退出

  • 线程A执行

  Node<E> q = p.next;

此时,p(a).next 指向自己(等待被GC), 进入else if (p == q)分支,线程A退出,经过一番执行后,最后得到的状态,如下:

进入else if (p == q)分支的情况,只会发生在polloffer共存的情况下。

2.4 peek

获取首个有效的节点,并返回

public E peek() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
if (item != null || (q = p.next) == null) {
updateHead(h, p);
return item;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}

peekpoll的操作类似,这里就贴一下代码就是了。

3. 总结

ConcurrentLinkedQueue是使用非阻塞的方式保证线程的安全性,在设置关系到整个Queue结构的变量时(这些变量都被volatile修饰),都使用CAS的方式对它们进行赋值。

  • size方法是线程不安全的,返回的结果可能不准确

关于“两跳机制”(自己取得名字),

Both head and tail are permitted to lag. In fact, failing to update them every time one could is a significant optimization (fewer CASes). As with LinkedTransferQueue (see the internal documentation for that class), we use a slack threshold of two; that is, we update head/tail when the current pointer appears to be two or more steps away from the first/last node.

Since head and tail are updated concurrently and independently, it is possible for tail to lag behind head (why not)? -- ConcurrentLinkedQueue

大致意思,headtail允许被延迟设置。不是每次更新它们是一个重大的优化,这样做就可以更少的CAS(这样在很多线程使用时,积少成多,效率更高)。它的延迟阈值是2,设置head/tail时,当前的结点离first/last有两步或更多的距离。 这就是“两跳机制

我们想不通的地方,可能是这个类或方法的一个优化的地方。向着大佬看齐~

4. 引用

Java多线程 39 - ConcurrentLinkedQueue详解,讲的非常好,上面的思路是跟着他来的

JAVA并发(4)-并发队列ConcurrentLinkedQueue的更多相关文章

  1. Java中的阻塞队列-ConcurrentLinkedQueue

    http://ifeve.com/concurrentlinkedqueue/ 1.    引言 在并发编程中我们有时候需要使用线程安全的队列.如果我们要实现一个线程安全的队列有两种实现方式一种是使用 ...

  2. Java并发包源码学习系列:基于CAS非阻塞并发队列ConcurrentLinkedQueue源码解析

    目录 非阻塞并发队列ConcurrentLinkedQueue概述 结构组成 基本不变式 head的不变式与可变式 tail的不变式与可变式 offer操作 源码解析 图解offer操作 JDK1.6 ...

  3. Java编程的逻辑 (76) - 并发容器 - 各种队列

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  4. 【Java并发】并发队列与线程池

    并发队列 阻塞队列与非阻塞队 ConcurrentLinkedQueue BlockingQueue ArrayBlockingQueue LinkedBlockingQueue PriorityBl ...

  5. 多线程高并发编程(11) -- 非阻塞队列ConcurrentLinkedQueue源码分析

    一.背景 要实现对队列的安全访问,有两种方式:阻塞算法和非阻塞算法.阻塞算法的实现是使用一把锁(出队和入队同一把锁ArrayBlockingQueue)和两把锁(出队和入队各一把锁LinkedBloc ...

  6. java并发:阻塞队列

    第一节 阻塞队列 1.1 初识阻塞队列 队列以一种先进先出的方式管理数据,阻塞队列(BlockingQueue)是一个支持两个附加操作的队列,这两个附加的操作是:在队列为空时,获取元素的线程会等待队列 ...

  7. 聊聊并发(七)——Java中的阻塞队列

    3. 阻塞队列的实现原理 聊聊并发(七)--Java中的阻塞队列 作者 方腾飞 发布于 2013年12月18日 | ArchSummit全球架构师峰会(北京站)2016年12月02-03日举办,了解更 ...

  8. Java并发编程-阻塞队列(BlockingQueue)的实现原理

    背景:总结JUC下面的阻塞队列的实现,很方便写生产者消费者模式. 常用操作方法 常用的实现类 ArrayBlockingQueue DelayQueue LinkedBlockingQueue Pri ...

  9. Java并发编程笔记之ConcurrentLinkedQueue源码探究

    JDK 中基于链表的非阻塞无界队列 ConcurrentLinkedQueue 原理剖析,ConcurrentLinkedQueue 内部是如何使用 CAS 非阻塞算法来保证多线程下入队出队操作的线程 ...

  10. [Java并发] AQS抽象队列同步器源码解析--锁获取过程

    要深入了解java并发知识,AbstractQueuedSynchronizer(AQS)是必须要拿出来深入学习的,AQS可以说是贯穿了整个JUC并发包,例如ReentrantLock,CountDo ...

随机推荐

  1. kubectl cp 从k8s pod 中 拷贝 文件到本地

    请查看官方的说明 kubectl cp --help 官方说使用cp , pod里需要有tar命令 从k8s pod 中 拷贝 文件到本地 这是我使用的命令 kubectl exec redis-6c ...

  2. 【cypress】3. 编写第一个测试

    当环境安装好了之后,就可以着手尝试第一个测试的编写了. 一.新建一个文件 在你的项目下的cypress/integration文件夹中创建一个新文件sample_spec.js,我这里直接在webst ...

  3. OO Unit4总结 & 结课总结

    OO Unit4总结 & 结课总结 OO课Unit4 UML解析应用技术回顾 BUAA.1823.邓新宇 2020/6/19 总结本单元三次作业的架构设计 本单元的架构设计主要是两方面. 一方 ...

  4. 11- APP性能测试GT工具的使用

    对性能测试来说有服务端的性能与客户端(APP)的性能. GT简介 1.GT(随身调)是APP的随身调测平台,它是直接运行在手机上的"集成调试环境"(IDTE) 2.利用GT,仅凭一 ...

  5. spring boot 或 spring 集成 atomikos jta 完成多数据源事务管理

    前言:对于事务,spring 不提供自己的实现,只是定义了一个接口来供其他厂商实现,具体些的请看我的这篇文章: https://www.cnblogs.com/qiaoyutao/p/11289996 ...

  6. Intel汇编语言程序设计学习-第二章 IA-32处理器体系结构-下

    2.2  IA-32处理器体系结构 如前所述,IA-32是指始于Intel386直到当前最新的奔腾4的系列的处理器(额...这本书是什么时候写的啊,表示现在应该是I7啊),在IA-32的发展过程中,I ...

  7. ThinkPHP5 Apache / IIs环境下 URL重写

    thinkPHP5新版本 隐藏index.php隐藏index.php 都写好了 public 隐藏 独立主机可以直接把根目录指向public下 虚拟主机可以把public下的index.php放到根 ...

  8. phpstorm中加上符号($,括号等)后搜索不到

    Ctrl+F右边选中这个Regex后带上符号就搜索不到,不要勾选这个就可以带符搜索了

  9. React 代码共享最佳实践方式

    任何一个项目发展到一定复杂性的时候,必然会面临逻辑复用的问题.在React中实现逻辑复用通常有以下几种方式:Mixin.高阶组件(HOC).修饰器(decorator).Render Props.Ho ...

  10. 使用FileStream读写数据

    这节讲一下使用FileStream读写数据,这是一个比较基础的流. FileStream类只能处理原始字节,所以它可以处理任何类型的文件. 先看一下它的构造方法: FileStream fs = ne ...