一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情。
基于JDK1.8详细介绍了ConcurrentLinkedQueue的底层源码实现,包括同步原理、入队操作、出队操作、获取操作等。
1 ConcurrentLinkedQueue的概述
public class ConcurrentLinkedQueue< E > extends AbstractQueue< E > implements Queue< E >, Serializable
Java编程中我们常常会使用到队列结构,最常见的就是LinkedList,然而它并不是线程安全的,如果我们需要在并发编程中安全的使用队列,那么LinkedList就不能使用了。
JDK1.5的时候出现的JUC包中提供了很多线程安全并且并发性能较好的容器,其中就有线程安全的队列的实现,它提供了两种并发队列的实现,一种是阻塞式的,比如ArrayBlockingQueue、LinkedBlockingDeque等;另一种一种是非阻塞式的,比如ConcurrentLinkedQueue、ConcurrentLinkedDeque等。它们的区别就是阻塞式队列在无法获取或者存入数据时,取线程或者存线程将会被阻塞直到可以取到数据或者可以存入数据。而非阻塞式的的队列则和普通队列一样,无论有没有取到数据获取无论有没有存入数据,相应的线程都不会被阻塞,可能会返回null或者false。
阻塞式队列通常使用锁来实现,因为需要线程的挂起和唤醒,而非阻塞式队列则只需要使用CAS来保证单个变量复合操作的同步和安全即可。因此非阻塞式对象在高并发的情况下通常性能都非常良好。
ConcurrentLinkedQueue就是JDK1.5诞生的非阻塞式的线程安全的队列,底层使用链表结构,因此可以存储的元素数量理论上是无限的(有内存限制),被称为“无界非阻塞队列”。
使用先进先出的规则(FIFO)存取元素,元素被添加到队列尾部,同时从队列头部取出元素。使用volatile+CAS 无锁算法来保证出入队时操作链表头、尾单个结点的线程安全即可。
和很多并发容器一样,ConcurrentLinkedQueue不允许null元素。
实现了Serializable接口,支持序列化;没有实现Cloneable接口,不支持克隆。
2 ConcurrentLinkedQueue的实现
2.1 基本结构
ConcurrentLinkedQueue作为链表实现的队列,存在一个head结点和tail结点。
同时Node内部类作为结点,Node中具有item属性存储值,next属性存储下一个结点的引用,结点与结点之间就是通过这个next关联起来,因此ConcurrentLinkedQueue内部的链表是一张单链表。
上面的属性都是使用volatile修饰的,能够保证可见性。对于结点的操作都是采用CAS无锁算法操作的,性能比较好。新元素会被插入队列末尾,出队时从队列头部获取一个元素。
下面是一些主要属性和方法,可能注释比较生硬,后续源码中会讲到,现在不必深究:
public class ConcurrentLinkedQueue<E> {
/**
* head指向的结点可能是哨兵结点(没存放数据),也可能是存放数据的结点
* head指向的结点可能不是真正的头结点
* head永远不为null
* 具有volatile的语义
*/
private transient volatile Node<E> head;
/**
* tail指向的结点可能是哨兵结点(没存放数据),也可能是存放数据的结点
* tail指向的结点可能不是真正的尾结点
* tail永远不为null
* 具有volatile的语义
*/
private transient volatile Node<E> tail;
/**
* 结点内部类
*/
private static class Node<E> {
/**
* 值域,具有volatile的语义
*/
volatile E item;
/**
* 下一个结点的引用,具有volatile的语义
*/
volatile Node<E> next;
/**
* 构造器
*/
Node(E item) {
UNSAFE.putObject(this, itemOffset, item);
}
//一系列CAS操作
/**
* CAS的替换item值,从cmp变成val
*
* @param cmp 预期值
* @param val 新值
* @return true 成功 false 失败
*/
boolean casItem(E cmp, E val) {
return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
}
/**
* volatile的延迟的设置next属性的值,插入StoreStore屏障而不是StoreLoad屏障,开销较小
*
* @param val
*/
void lazySetNext(Node<E> val) {
UNSAFE.putOrderedObject(this, nextOffset, val);
}
/**
* CAS的替换next值,从cmp变成val
*
* @param cmp 预期值
* @param val 新值
* @return true 成功 false 失败
*/
boolean casNext(Node<E> cmp, Node<E> val) {
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
/**
* CAS操作需要用到的Unsafe
*/
private static final sun.misc.Unsafe UNSAFE;
/**
* 当前Node结点的item属性偏移量
*/
private static final long itemOffset;
/**
* 当前Node结点的next属性偏移量
*/
private static final long nextOffset;
static {
//获取item和next 非静态属性在它的类的存储分配中的位置(偏移量),方便后续方法的调用
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);
}
}
}
}
复制代码
2.2 构造器
从下面的构造器中能看出,head和tail不会指向null。
2.2.1 ConcurrentLinkedQueue
public ConcurrentLinkedQueue()
创建一个空的ConcurrentLinkedQueue实例,head和tail默认指向一个新建的item为null的Node结点(哨兵结点)。
/**
* 创建一个空的ConcurrentLinkedQueue实例
*/
public ConcurrentLinkedQueue() {
//head和tail默认指向一个item为null的Node结点。
head = tail = new Node<E>(null);
}
复制代码
2.2.2 ConcurrentLinkedQueue( c )
ConcurrentLinkedQueue(Collection<? extends E> c)
创建一个包含指定集合全部元素的ConcurrentLinkedQueue实例,按照指定集合的迭代器遍历顺序添加元素。
如果指定的集合或其任何元素为空,则抛出NullPointerException。
/**
* 创建一个包含指定集合全部元素的ConcurrentLinkedQueue实例
* 按照指定集合的迭代器遍历顺序添加
*
* @param c 指定集合
* @throws NullPointerException 如果指定的集合或其任何元素为空
*/
public ConcurrentLinkedQueue(Collection<? extends E> c) {
//新建变量h,最后会作为头结点;t,最后会作为尾结点
Node<E> h = null, t = null;
/*迭代遍历c,建立链表*/
for (E e : c) {
//null检查,如果为null则抛出NullPointerException
checkNotNull(e);
//新建立Node,包含值当前遍历值
Node<E> newNode = new Node<E>(e);
/*第一次循环时,h == null为真,那么将会为h和t赋初初始值*/
if (h == null)
h = t = newNode;
/*后续循环时,走else的逻辑*/
else {
//设置t的next引用
t.lazySetNext(newNode);
//t等于新建结点,这样就建立起来了一张链表
t = newNode;
}
}
//循环结束如果h为null,说明集合c是空的
if (h == null)
//此时,类似于空构造器,h和t指向一个item为null的Node结点。
h = t = new Node<E>(null);
//h为head
head = h;
//t为tail
tail = t;
}
/**
* null检查
*
* @param v 元素
*/
private static void checkNotNull(Object v) {
if (v == null)
throw new NullPointerException();
}
复制代码
2.3 入队操作
2.3.1 offer方法
public boolean offer(E e)
将指定元素插入此队列的尾部。在成功时返回 true,由于队列无限制,此方法永远不会返回false。如果指定的元素为空,则抛出 NullPointerException。
大概步骤为:
- null检查,如果e为null则抛出NullPointerException;
- 新建结点newNode,包括item指向参数e;
- 首先使用t保存当前的tail,p保存t;然后开启一个死循环,这是CAS无锁算法的常见套路,循环(自旋)重试直到成功插入,没有使用锁:
- 获取p的next引用q,即此时tail的next。
- 如果q为null,说明p就是真正的尾结点,正常情况。尝试CAS的将p的next引用从null设置为newNode:
- 如果CAS成功,并且此时p不等于t,即此时原tail指向的结点t不是真正的尾结点,那么继续尝试CAS设置tail指向新结点newNode,此时CAS失败也没关系。如果p等于t,即原来的tail指向的真正的尾结点,那么此时没有更新tail的引用。
- CAS失败则进入下一次循环。
- 否则,如果p等于q,此时tail结点的next指向自身并且与原链表分离,这是被删除的结点的特殊情况,需要重新查找尾结点。重新查找尾结点并为p赋值之后进入下一次循环(此时p可能h是尾结点,也肯能是头结点);
- 否则,q不等于null,说明p不是真正的尾结点,那么寻找真正的尾结点并为p赋值之后进入下一次循环。
总结起来,入队列方法主要步骤是:
- 通过tail查找真正的尾结点,在ConcurrentLinkedQueue中只有结点的next引用为null的结点才算是真正的尾结点,tail可能指向尾结点,也可能指向队列中间的某个结点,也可能指向被删除了的结点;
- 将新结点链接到原尾结点后面成为新的尾结点,然后根据条件判断是否调用casTail方法,最终返回true。
/**
* 在此队列的尾部插入指定的元素。 由于队列是无边界的,此方法将永远不会返回false
*
* @param e 指定元素
* @return 插入成功返回true,只会返回true
* @throws NullPointerException 如果指定的元素为空
*/
public boolean offer(E e) {
/*1 null检查*/
checkNotNull(e);
//新建Node
final Node<E> newNode = new Node<E>(e);
//2 首先使用t保存当前的tail,p保存t;然后开启一个死循环,这是CAS无锁算法的常见套路
for (Node<E> t = tail, p = t; ; ) {
//获取p的next引用q,即此时tail的next
Node<E> q = p.next;
/*
* 2.1 如果q为null,说明p就是真正的尾结点,正常情况
* 在ConcurrentLinkedQueue中,唯一能够确定尾结点的方法就是如果一个结点的next引用指向null,那么该结点就是尾部结点
*/
if (q == null) {
//尝试CAS的将p的next引用从null设置为新Node
if (p.casNext(null, newNode)) {
//CAS成功
//如果p不等于t,说明循环了多次
if (p != t) // hop two nodes at a time
//尝试CAS设置tail指向新结点,失败也没关系,因为可能其线程已经设置了tail结点,后续新调用的offer而方法也会自动查找真正的尾结点
casTail(t, newNode); // Failure is OK.
//CAS成功,返回true,该死循环的唯一出口就是这里,即offer方法只会返回true
//可以看到如果p等于t,即原来的tail指向的真正的尾结点,那么此时没有更新tail的引用,因此尾结点具有滞后性,即tail指向的结点 不一定是尾结点
return true;
}
//CAS失败的线程,进行下一次循环
}
/*
* 2.2 否则,如果p等于q,此时tail结点的next指向自身并且与原链表分离,这是在处理被删除的结点的特殊情况
* poll的时候会把老的head的next引用变为自引用
* 当tail不是最后的结点时,如果此时执行出队列操作,可能将tail指向的结点移除了,此时需要重新查找真正的尾结点
* 之后继续下一次循环
*/
else if (p == q)
//重新查找尾结点
/*如果原来的t不等于tail(说明tail有变化,同时t赋值为最新的tail),那么p=t;否则p=head*/
p = (t != (t = tail)) ? t : head;
/*
* 2.3 否则,q不等于null,说明p不是真正的尾结点,那么寻找真正的尾结点,正常情况
* 之后继续下一次循环
*/
else
//重新查找真正的尾结点
/*如果此时p不等于t 并且 原来的t不等于tail(说明tail有变化,同时t赋值为最新的tail),那么p=t;否则p=q*/
p = (p != t && t != (t = tail)) ? t : q;
}
}
}
复制代码
2.3.2 add方法
public boolean add(E e)
将指定元素插入此队列的尾部。在成功时返回 true,由于队列无限制,此方法永远不会返回false。如果指定的元素为空,则抛出 NullPointerException。
/**
* 在此队列的尾部插入指定的元素。 由于队列是无边界的,此方法将永远不会返回false
*
* @param e 指定元素
* @return 插入成功返回true,只会返回true
* @throws NullPointerException 如果指定的元素为空
*/
public boolean add(E e) {
//内部直接调用的offer方法
return offer(e);
}
复制代码
2.4 出队操作
2.4.1 poll方法
public E poll()
在队列头部获取并且移除一个元素,如果队列为空则返回 null。该方法不会阻塞,而是循环使用CAS尝试,大概步骤为:
- 开启一个死循环:
- 使用变量h保存此时的head,变量p=h,设置q变量,内部再开启一个死循环:
- 使用item保存p的item;
- 如果item不为null,并且尝试CAS将p的值从item设置为null成功,表示出队列成功。如果p不等于h(类似于offer方法),即此时原head指向的结点h不是真正的头结点,那么尝试CAS的将head的指向从h变成p.next(如果p.next不为null)或者p(如果p.next为null)。并且,将原head结点h的next引用指向自己,这样就将原头结点和队列解除了关系,将会被GC回收。返回item,方法结束。
- 否则,表示item为null,或者出队列失败,或者遇到了自引用的情况。如果q = p.next等于null,表示此时队列为null,返回null,方法结束。
- 否则,如果p=q,表示遇到了自引用的情况。该线程获取到了被别的线程移除的,但是还没有改变head引用关系的结点,那么结束本次外层循环,重新获取最新的head的引用,重新开始内层循环。
- 否则,将p赋值为q。即排除item为null的结点,寻找真正的头结点,即item不为null的第一个结点。结束本次循环,继续下次循环。
可以看到,在出队列的时候,会将原head的neext指向自身,这就是所谓的“自引用”。另外这里的更新head的指向,也是类似于offer方法并不是每一次都更新head引用的指向,如果本次操作时,最先获取的h就是头结点并且操作成功,那么不会更新head引用指向。
总结起来,出队列方法主要步骤是:
- 通过head查找真正的头结点,在ConcurrentLinkedQueue中只有从头到尾第一个item不为null的结点才算是真正的头结点,head可能指向头结点,也可能指向一个item为null的哨兵结点;
- 没找到有效的头结点那么就调用updateHea方法并返回null;找到之后首先将原头结点的item置为null,然后根据条件判断是否调用updateHead方法,最后返回item。
/**
* @return 获取并移除此队列的头,如果此队列为空,则返回 null。
*/
public E poll() {
restartFromHead:
/*1 开启一个死循环*/
for (; ; ) {
/*
* 2 使用变量h保存此时的head,变量p=h,设置q变量
* 内部再开启一个死循环
*/
for (Node<E> h = head, p = h, q; ; ) {
//使用item保存p的item
E item = p.item;
/*2.1 如果item不为null,并且尝试CAS将值从item设置为null成功,表示出队列成功*/
if (item != null && p.casItem(item, null)) {
// Successful CAS is the linearization point
// for item to be removed from this queue.
//如果p不等于h,那说明循环多次,head已经变了
//这里类似于offer方法中,每两次更新tail指向的操作
if (p != h) // hop two nodes at a time
//尝试CAS的将head的指向从h变成p.next(如果p.next不为null)或者p(如果p.next为null)
//并且,将原head结点h的next引用指向自己,这样就将原头结点和队列解除了关系,将会被GC回收
updateHead(h, ((q = p.next) != null) ? q : p);
//返回item,方法结束
return item;
}
/*
* 2.2 否则,表示item为null,或者出队列失败,或者遇到了自引用的情况
* 如果q = p.next等于null,表示此时队列为null
*/
else if ((q = p.next) == null) {
//尝试CAS的将head的指向从h变成p
updateHead(h, p);
//返回null
return null;
}
/*
* 2.3 否则,如果p=q,表示遇到了自引用的情况
* 该线程获取到了 被别的线程移除的,但是还没有改变head引用关系的结点
* 那么结束本次外层循环,重新获取最新的head的引用,重新开始内层循环。
*/
else if (p == q)
continue restartFromHead;
/*
* 2.4 否则,将p赋值为q
* 即排除item为null的结点,寻找真正的head结点,即item不为null的第一个结点
*/
else
p = q;
}
}
}
/**
* p 作为新的链表头部(通过casHead()实现),而原有的head 就被设置为哨兵(通过lazySetNext()方法实现)。
*
* @param h 原头结点
* @param p 新头结点
*/
final void updateHead(Node<E> h, Node<E> p) {
//如果h不等于h 并且 尝试CAS的将head引用从原值h指向新值p成功
if (h != p && casHead(h, p))
//那么将原头结点的next指向自己,将会被回收
h.lazySetNext(h);
}
/**
* CAS的将head引用的指向从cmp变成val
*
* @param cmp 预期值
* @param val 新值
* @return true 成功 false 失败
*/
private boolean casHead(Node<E> cmp, Node<E> val) {
return UNSAFE.compareAndSwapObject(this, headOffset, cmp, val);
}
/**
* Node内部类中的方法
* 将next引用指向指定Node
*
* @param val 指定Node
*/
void lazySetNext(Node<E> val) {
UNSAFE.putOrderedObject(this, nextOffset, val);
}
复制代码
2.4.2 remove方法
public boolean remove(Object o)
获取并移除此队列的头。此方法与 poll 唯一的不同在于:此队列为空时将抛出一个NoSuchElementException异常。
/**
* 获取并移除此队列的头。此方法与 poll 唯一的不同在于:此队列为空时将抛出一个NoSuchElementException异常。
*
* @return 头元素
* @throws NoSuchElementException 如果队列为空
*/
public E remove() {
//内部直接调用poll方法
E x = poll();
//判空
if (x != null)
return x;
else
throw new NoSuchElementException();
}
复制代码
2.5 过程详解
在上面的我们学习了最基本的出队列和入队列的原理以及源码,可以看到,虽然源码不是很多,但是却考虑到了非常多的复杂的情况,这些情况可能光靠语言难以描述,我们准备以图文并茂的方法帮助大家了解各种情况以及处理方式!
2.5.1 单线程offer
单个线程下的offer方法比较好理解,先看单线程的主要是为了理解后面的多线程的情况
2.5.1.1 初始化
新建ConcurrentLinkedQueue实例时,此时的结构如下:
2.5.1.2 插入第一个元素a
在offer方法中第一次循环时,t = tail,p = t,q = p.next,引用指向如下:
然后判断此时q==null肯定为true,然后将结点a添加到p结点后面:
继续判断此时p != t为false,那么不会改变tail的指向,此时的结构如下:
2.5.1.3 插入第二个元素b
在offer方法中第一次循环时,t = tail,p = t,q = p.next,引用指向如下:
然后判断此时q == null肯定为false,继续判断p==q也为false,然后进入最后一个else寻找真正的尾结点:判断p!=t即为false,最终p=q,第一次循环结束:
开始第二次循环,q = p.next,即此时q=null:
然后判断此时q == null肯定为true,然后将新结点b添加到p结点后面:
继续判断此时p != t为true,那么改变tail指向新结点b,此时的结构如下:
2.5.1.4 插入第三个元素c
在offer方法中第一次循环时,t = tail,p = t,q = p.next,引用指向如下:
然后判断此时q == null肯定为true,然后将结点c添加到p结点后面:
继续判断此时p != t为false,那么不会改变tail的指向,此时的结构如下:
2.5.1.5 插入第四个元素d
在offer方法中第一次循环时,t = tail,p = t,q = p.next,引用指向如下:
然后判断此时q == null肯定为false,继续判断p==q也为false,然后进入最后一个else寻找真正的尾结点:判断p!=t即为false,最终p=q,第一次循环结束:
开始第二次循环,q = p.next,即此时q=null:
然后判断此时q == null肯定为true,然后将新结点d添加到p结点后面:
继续判断此时p != t为true,那么改变tail指向新结点d,此时的结构如下:
总结起来,单线程offer操作会每隔两个元素更新一次tail,而head则不变:
即每次CAS插入元素之后,如果此时原tail指向的结点t不是真正的尾结点了,那么调用updateHead设置tail指向新结点newNode;如果t还是真正的尾结点,那么就不会改变tail的引用!
2.5.2 单线程poll
我们以上面的案例为例子,下面来看看单线程poll的执行流程:
2.5.2.1 第一次poll
第一次内层循环开始时,h = head, p = h:
然后,item=p.item,此时肯定item为null,因此第一个if判断不成立,进入第二个else if,q = p.next:
此时q肯定也不为null,因此第二个else if判断不成立,进入第三个else if ,p==q也肯定不成了,它们不相等,因此进入最后一个else,将p赋值为q:
然后开始第二次循环。item=p.item,此时肯定不为null,实际上从第一次循环到这里的步骤就是在寻找真正的头结点的过程:从head开始往后,第一个item不为null的结点就是头结点,然后尝试CAS的将a结点的item置为null,多线程下不一定会成功,单线程下会成功。
CAS成功之后,进入if代码块中。继续判断p!=h肯定为true,表示此时的head指向的结点不是真正的头结点,那么需要重新设置头结点。
q= p.next,即q指向结点b,肯定部位null,因此updateHead方法中,将head指向q代表的结点,即结点b,然后h的next指向自己,将会被GC回收:
返回item,第一次poll结束。
2.5.2.2 第二次poll
第一次内层循环开始时,h = head, p = h:
然后,item=p.item,此时肯定item不为null,然后尝试CAS的将b结点的item置为null,多线程下不一定会成功,单线程下会成功。
CAS成功之后,进入if代码块中。继续判断p!=h肯定为false,表示此时的head指向的结点是真正的头结点,那么不需要重新设置头结点。
返回item,方法结束。我们看到如果head指向的就是真正的头结点时,poll还是比较轻松的,并且之后不需要调整head的引用关系,而是等到下一次poll时再调整。
2.5.2.1 第三次poll
第一次内层循环开始时,h = head, p = h:
然后,item=p.item,此时肯定item为null,因此第一个if判断不成立,进入第二个else if,q = p.next:
此时q肯定也不为null,因此第二个else if判断不成立,进入第三个else if ,p == q也肯定不成立了,它们不相等,因此进入最后一个else,将p赋值为q:
然后开始第二次循环。item=p.item,此时肯定不为null,然后尝试CAS的将c结点的item置为null,多线程下不一定会成功,单线程下会成功。
CAS成功之后,进入if代码块中。继续判断p!=h肯定为true,表示此时的head指向的结点不是真正的头结点,那么需要重新设置头结点。
q= p.next,即q指向结点b,肯定部位null,因此updateHead方法中,将head指向q代表的结点,即结点d,然后h的next指向自己,将会被GC回收:
2.5.2.2 第四次poll
第一次内层循环开始时,h = head, p = h:
然后,item=p.item,此时肯定item不为null,然后尝试CAS的将d结点的item置为null,多线程下不一定会成功,单线程下会成功。
CAS成功之后,进入if代码块中。继续判断p!=h肯定为false,表示此时的head指向的结点是真正的头结点,那么不需要重新设置头结点。
返回item,方法结束。我们看到如果head指向的就是真正的头结点时,poll还是比较轻松的,并且之后不需要调整head的引用关系,而是等到下一次poll时再调整。
2.5.2.1 第五次poll
第一次内层循环开始时,h = head, p = h:
然后,item=p.item,此时肯定item为null,因此第一个if判断不成立,进入第二个else if,q = p.next:
此时q==null为true,那么第二个else if成立,即此时队列为空,调用updateHead方法,由于h == p为true,因此实际上什么也不做,之后返回null,方法结束。
我们发现此时似乎又一次被删除的结点还存在一个next引用关系而没有被删除。那是因为在上面的分析中,我们认为只要有结点引用关联就不会被GC回收,然而实际上现代Java虚拟机采用可达性分析算法来分析垃圾,因此,在上面的队列中,对于那些不能通过head和tail的引用链到达、在方法栈的本地变量表中也没有外部引用的结点,将会在可达性分析算法中被标记为垃圾并在一次GC中被直接回收!因此实际上,上面的灰色的结点在GC时应该都会被及时回收,此时真正的结构如下:
即,我们又回到了最初始的地方!现在我们来看看单线程poll时的数据结构的变化:
即,每次poll之前,如果head指向真正的头结点,那么poll之后不会改变head的引用,否则将更新head的引用。
2.5.3 单线程交替
如果某个队列初始结构如下:
那么在第五次poll之后,它的结构如下:
此时,如果我们进行offer添加结点f操作:
在offer方法中第一次循环时,t = tail,p = t,q = p.next,引用指向如下:
然后进入第一个if判断 q == null 肯定为false,进入第二个if else继续判断 p == q,返回true,这就是我们在offer方法中所说的“自引用”的情况。
此时我们需要重新查找新的尾结点或者头结点。此时t还是等于tail,那么p指向head,继续下一次循环。
第二次循环时,q = p.next明显为null,然后就可以在head指向的结点后面添加新结点了,并且添加之后,p!=t为true,此时将tail更新为指向新添加结点,最终结构为:
2.5.4 多线程并发
在上面我们讲了单线程单方法或者单线程交替执行时的各种情况,如果是在多线程环境下,首先上面情况肯定是全都会存在的,然后由于多线程并发操作,会导致更多的情况发生。
在offer方法中,单线程环境下我们只见识到了判断的三种情况都可能发生:第一个if (q == null)为真,说明tail指向真正的尾结点;第二个else if (p == q)为真,说明此时tail不是真正的尾结点并且tail指向的结点被删除了,需要向后查找真正的尾结点;第三个else为真,说明tail此时指向一个哨兵结点,需要向后查找真正的尾结点。在多线程下上面的情况自然也不可避免,源码中的各种判断也都是为多线程准备的。
在poll方法中,单线程下我们只见识到了:第一个if (item != null && p.casItem(item, null))为真,说明此时head指向真正的头结点;第二个else if ((q = p.next) == null)为真,说明此时队列为空。直接返回;最后一个else为真,说明此时队列不为空,并且head指向的是一个一个哨兵结点,需要向后查找真的的头结点。
在并发的时候上面三种情况自然都能发生,源码中的各种判断也都是为多线程准备的。但是在单线程下唯独没有见到第三种情况else if (p == q)为真,即poll的时候出现“自引用”的情况,因为这种情况只能在并发环境下才能发生,下面我们来看看这种情况到底是怎么发生的!
首先借用“补充交替情况”部分的初始条件,在第四次删除之后,队列结构应该如下:
此时,如果有两个线程thread1和thread2同时进行第五次删除,并且都刚刚进入了第一次循环,那么相关结构和引用关系如下:
注意,我们图中的p、q、item都是方法中的局部变量,在栈空间中属于线程私有,而head和tail两个引用变量则是两个线程共享的。
假设thread1先一步执行,将第五次删除方法走完,并退出方法,此时只剩下thread2,那么结构如下:
如果此时thread2获得了cpu的执行权,但是由于thread2已经进入了循环体中,因此会直接走判断逻辑,此时item肯定为null因此第一个if不成立,然后走第二个else if:q = p.next,由于此时p指向结点的next指向自身,因此q也指向自身:
此时q==null肯定也不成立,然后走第三个else if,此时p == q明显成立,这就是所谓的在poll时出现的“自引用”情况,当多线程并发的删除同一个元素结点时就有可能发生。
代码中的解决办法是:结束本次大循环,继续下一次大循环。我们知道在大循环中会开启小循环,此时首先做的就是重新获取head并赋值:h = head, p = h,这样thread2的引用关系就变成了如下样式:
即找到了目前head的所在位置,进入小循环之后明显会走else if ((q = p.next) == null)的逻辑,最终返回null。
其实多线程下还有更多的情况会发生,但是最终都能够通过offer或者poll中的少量的循环CAS操作给处理掉,这也不得不感叹并发编程大师Doug Lea缜密绝伦的思想!
比如offer方法中的最后一个else查找真正尾结点的代码:
p = (p != t && t != (t = tail)) ? t : q;
复制代码
如果p不等于t 并且 t原来指向的结点 不等于 此时的tail指向的结点(同时将t赋值为最新的tail),这说明tail的指向已经改变了,因此p=t,继续下一次循环。这一行代码就像第一次进入内层for循环之前初始化操作t = tail, p = t一样,非常精妙。
另外,offer方法中,在结点添加完毕之后,如果最开始获取的t在还是等于p,那么说明这个tail指向的就是真正的尾结点,那么插入数据之后不更新tail的引用指向(自引用时,它可能直接会找到head并在其后入队);在poll方法中,在出队列成功之后,如果最开始获取的h还是等于p,那么此时这个head指向的就是是真正的头结点,那么取出数据之后不更新head的引用指向。
这样的设计避免了每次的入队或者出队都需要更新tail和head的引用指向的逻辑,取而代之的是某些线程可能会多循环几次来查找真正的尾结点或者头结点,使用多次的查找代替频繁的写的原因是head和tail都是volatile类型的变量,对于volatile类型的变量来说,写的开销远远大于读的开销,在底层使用了#lock指令,频繁的写volatile可能由于MESI缓存一致性协议而导致锁总线占用过多的流量而造成总线风暴,所以不必每一次都写volatile可以提升入队列和出队列的性能!
2.6 获取操作
2.6.1 peek方法
获取但不移除此队列的头;如果此队列为空,则返回 null。 大概步骤为:
- 开启一个死循环:
- 使用变量h保存此时的head,变量p=h,设置q变量,内部再开启一个死循环:
- 使用item保存p的item;
- 如果item不为null,表示此结点就是头结点,或者如果q = p.next并且q为null,表示此时队列为空,这两种情况满足一种都可以返回,之后尝试使用updateHead更新head引用的指向。返回item,方法结束。
- 否则,如果p=q,表示遇到了自引用的情况。该线程获取到了被别的线程移除的,但是还没有改变head引用关系的结点,那么结束本次外层循环,重新获取最新的head的引用,重新开始内层循环。
- 否则,将p赋值为q。即排除item为null的结点,寻找真正的头结点,即item不为null的第一个结点。结束本次循环,继续下次循环。
- 使用变量h保存此时的head,变量p=h,设置q变量,内部再开启一个死循环:
/**
* 获取但不移除此队列的头;如果此队列为空,则返回 null。
* 内部代码和poll差不多,只是这里更加简单,不需要移除数据
*
* @return 队列的头;如果此队列为空,则返回 null。
*/
public E peek() {
/*一个死循环*/
restartFromHead:
for (; ; ) {
//h=head p=h
//再次开启一个死循环,从head开始遍历
for (Node<E> h = head, p = h, q; ; ) {
//获取item
E item = p.item;
/*
* 如果item不为null,说明有值,可以返回
* 或者 p.next为null,说明对列为null,也可以返回
*/
if (item != null || (q = p.next) == null) {
/*
* 尝试更新原head的引用指向,更新为p,只有最开始h指向的结点不是真正的头结点时才会发生
* 如果更新成功那么原h指向的结点将会发生自引用,而被移除底层链表;
* 同时,其他next属性指向最新head引用的结点的也将会因为GC roots不可达而被回收
*/
updateHead(h, p);
//返回item
return item;
/*
* 否则,如果p==q,说明遇到了自引用
* 那么结束本次大循环,重新开始内部小循环,查找最新的head引用指向
*/
} else if (p == q)
continue restartFromHead;
/*否则,说明遇到了哨兵结点,p=q继续向下查找真正的队列的头*/
else
p = q;
}
}
}
/**
* p 作为新的链表头部(通过casHead()实现),而原有的head 就被设置为哨兵(通过lazySetNext()方法实现)。
*
* @param h 原头结点
* @param p 新头结点
*/
final void updateHead(Node<E> h, Node<E> p) {
//如果h不等于h 并且 尝试CAS的将head引用从原值h指向新值p成功
if (h != p && casHead(h, p))
//那么将原头结点的next指向自己,将会被回收
h.lazySetNext(h);
}
复制代码
peek的代码与poll类似另外,只是少了移除操作。在调用peek方法返回之前,有头结点还是对列为null的情况,都会调用updateHead。这里的updateHead操作实际上是在清理真正的head之前的那些没有数据的哨兵结点数据,或者多一个哨兵结点数据。最终head会指向真正的头结点,或者一个哨兵结点(此时队列为空)。另外,在poll方法中,如果我们因为队列为空的原因而返回(q = p.next) == null,也会调用updateHead方法,同样是清理多余的哨兵结点数据。
比如,某时刻某个队列的结构如下,可以看到里面head指向的就是一个哨兵结点:
这时来了一条peek线程:
第一次内层循环开始时,h = head, p = h:
然后,item=p.item,进入第一个if,item等于null因此左边表达式不成立,q = p.next,q不等于null,因此右边表达式不成立,即第一个if不成立,进入第二个else if,很明显p==q不成立,那么进入第三个else,p = q:
进入第二次循环,此时item=p.item=a,进入第一个if,此时item不为null,那么可以返回,然后调用updateHead:
if (h != p && casHead(h, p))
h.lazySetNext(h);
复制代码
可以看到h != p为真,此时head指向p,即指向a结点,原head指向的哨兵结点将会构造成自引用而被清除。最终结构如下:
上面就是peek方法中所谓的清除多余的哨兵结点的逻辑。
2.6.2. element方法
public E element()
获取,但是不移除此队列的头,此队列为空时将抛出一个异常。
/**
* 获取,但是不移除此队列的头。此队列为空时将抛出一个异常。
*
* @return 队列头
* @throws NoSuchElementException 此队列为空
*/
public E element() {
//内部调用peek方法
E x = peek();
//判空
if (x != null)
return x;
else
throw new NoSuchElementException();
}
复制代码
2.7 其他操作
2.7.1 size方法
返回此队列中的元素数量。如果此队列包含的元素数大于 Integer.MAX_VALUE,则返回 Integer.MAX_VALUE。
这个方法的性能非常差,因为ConcurrentLinkedQueue中并没有专门用来数据的属性,每一次的size 都会遍历整个底层链表,首先需要获取真正的头结点,这类似于peek,然后从这个结点开始顺序向后遍历,时间复杂度为O(n)。
并且由于ConcurrentLinkedQueue支持增删改查完全并发,size获取的值很可能不是真实的值,因此返回的值没有太大意义。
/**
* 获取,但是不移除此队列的头。此队列为空时将抛出一个异常。
*
* @return 队列头
* @throws NoSuchElementException 此队列为空
*/
public E element() {
//内部调用peek
E x = peek();
//判空
if (x != null)
return x;
else
throw new NoSuchElementException();
}
/**
* 返回此队列中的元素数量近似值。如果此队列包含的元素数大于 Integer.MAX_VALUE,则返回 Integer.MAX_VALUE。
*
* @return 元素数量近似值,返回int类型
*/
public int size() {
//计数器
int count = 0;
//开启一个循环遍历
//首先通过first()获取第一个真正的头结点p,只调用一次该方法
//如果p为null则结束循环
for (Node<E> p = first(); p != null; p = succ(p))
//这里需要继续排除item为null的结点,因为p是随时都可能变动的
if (p.item != null)
// Collection.size() spec says to max out
//如果等于Integer.MAX_VALUE,那么直接返回了,否则遍历全部
if (++count == Integer.MAX_VALUE)
break;
return count;
}
/**
* 返回列表中的真正的头结点(item不为null的);如果队列为null,则返回null。
* 类似于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);
//返回结点对象或者null
return hasItem ? p : null;
} else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
/**
* 如果结点p在遍历时被删除,那么可能会构造成自引用结点,这里就是判断并处理并发时的自引用的情况
*/
final Node<E> succ(Node<E> p) {
Node<E> next = p.next;
//如果出现自引用,那么则返回最新head,重头开始,否则继续返回next结点
return (p == next) ? head : next;
}
复制代码
2.7.2 remove方法
public boolean remove(Object o)
从队列中移除指定元素的单个实例(如果存在多个,删除最前面的一个)。如果此队列由于调用而发生更改,则返回 true。另外判断元素是否存在使用的是equals方法。
/**
* 从队列中移除指定元素的单个实例(如果存在多个,删除最前面的一个)。
*
* @param o 需要被移除的元素
* @return {@code true} 如果此队列由于调用而发生更改,则返回 true。
*/
public boolean remove(Object o) {
//如果o不为null
if (o != null) {
Node<E> next, pred = null;
//开启一个循环
//首先p为设置为目前的真正的头结点
//如果p不等于null,则继续循环,否则结束循环
//一次循环完毕之后,pred=p,p=next。这里pred相当于前驱,next则是后继
for (Node<E> p = first(); p != null; pred = p, p = next) {
//移除标记
boolean removed = false;
//获取item
E item = p.item;
//如果不为null
if (item != null) {
//使用equals判断是否相等
//如果不相等
if (!o.equals(item)) {
//获取下一个结点赋值给next
next = succ(p);
//结束本次循环,继续下依次循环
continue;
}
//如果相等,那么尝试将结点p的item设置为null,使用removed接收返回值
//如果CAS成功返回true,此时就表示已经移除了
removed = p.casItem(item, null);
}
//获取下一个结点赋值给next
next = succ(p);
//如果pred不为null并且next不为null
if (pred != null && next != null) // unlink
//将pred的next引用指向next,失败了也没关系
pred.casNext(p, next);
//如果一次循环完毕,如果removed为true表示移除了,那么返回true
if (removed)
return true;
}
}
//返回false
return false;
}
}
复制代码
2.7.3 contains方法
public boolean contains(Object o)
如果此队列包含指定元素(使用equals方法判断),则返回 true。该方法类似于size方法,并不准确。
/**
* 如果此队列包含指定元素(使用equals方法判断),则返回 true。该方法类似于size方法,并不准确。
*
* @param o 指定元素
* @return 如果此队列包含指定元素(使用equals方法判断),则返回 true。
*/
public boolean contains(Object o) {
//如果o为null直接返回false
if (o == null) return false;
//开启一个循环
//首先p为设置为目前的真正的头结点,只操作一次
//如果p不等于null,则继续循环,否则结束循环
//一次循环完毕之后,p=succ(p),查找下一个结点
for (Node<E> p = first(); p != null; p = succ(p)) {
//获取item
E item = p.item;
//如果不为null 并且 和指定元素相等:equals返回true
if (item != null && o.equals(item))
//那么返回true
return true;
}
//没找到,返回false
return false;
}
复制代码
2.7.4 iterator方法
public Iterator iterator()
返回该concurrentLinkedQueue的迭代器,不会抛出ConcurrentModificationException,即可以并发修改。
源码还是比较简单的,从源码可以看出来,相比于普通集合,没有modCount机制,没有并发修改的检测,因此也就不会抛出ConcurrentModificationException异常,即这是一种“安全失败”机制——fail-safe,实际上JUC中的并发集合的迭代器都是安全失败的。但同时也造成了迭代器的“弱一致性”,即在迭代时对元素的增删改操作将可能不会反映出来。
另外迭代器的remove方法,仅仅是将结点的item置为null,并没有尝试移除结点,这样这个结点就变成了哨兵结点,只能等待其他线程在其它方法中一并移除。
/**
* 返回一个迭代器
*
* @return 一个迭代器实例
*/
public Iterator<E> iterator() {
return new Itr();
}
/**
* 迭代器的实现
*/
private class Itr implements Iterator<E> {
/**
* 要返回的下一个结点,如果为null则说明迭代完毕
*/
private Node<E> nextNode;
/**
* 要返回的下一个值
* 如果我们的hashNext判断有值,那么下面的next方法必须要有返回值
* 即使在hashNext和next方法调用之间该元素被删除了,也会返回被删除的元素
*/
private E nextItem;
/**
* 最后返回的没有被删除的结点,以支持删除,如果为null表示没有结点可以删除
*/
private Node<E> lastRet;
/**
* 构造器
*/
Itr() {
//调用advance,初始化相关属性
//计算下一次的nextNode、nextItem,本次的nextNode赋值给lastRet(null)
advance();
}
/**
* @return 返回当前的nextItem,同时计算下一次的nextNode、nextItem,本次的nextNode赋值给lastRet
*/
private E advance() {
//lastRet等于nextNode
lastRet = nextNode;
//获取nextItem的值,即要返回的值
E x = nextItem;
//pred保存目前的nextNode
//p保存最新的要返回的下一个结点
Node<E> pred, p;
/*如果nextNode为null,即下一个结点为null,那么重新查找要返回的下一个结点*/
if (nextNode == null) {
//查找最新头结点
p = first();
pred = null;
}
/*否则,nextNode不为null*/
else {
//pred保存nextNode
pred = nextNode;
//获取nextNode的下一个结点,或者head结点
p = succ(nextNode);
}
/*死循环*/
for (; ; ) {
//如果p为null,没有下一次要返回的节了
if (p == null) {
//引用置空
nextNode = null;
nextItem = null;
//返回x
return x;
}
//否则下一个要返回的结点p不为null
//获取iem
E item = p.item;
//如果item不为null
if (item != null) {
//nextNode等于p
nextNode = p;
//nextItem等于item
nextItem = item;
//返回x
return x;
}
/*否则,表示p是哨兵结点*/
else {
//返回p的next或者head
Node<E> next = succ(p);
//如果目前的nextNode(pred)不为null 并且 nextNode的next不为null
if (pred != null && next != null)
//那么pred的next指向next,即帮助去除中间的哨兵结点
pred.casNext(p, next);
//p等于next,继续下一次循环
p = next;
}
}
}
/**
* @return 如果仍有元素可以迭代,则返回 true。
*/
public boolean hasNext() {
//很简单,判断nextNode是否为null
return nextNode != null;
}
/**
* @return 返回迭代的下一个元素。
*/
public E next() {
//如果nextNode为null,那么抛出NoSuchElementException异常,表示没有可迭代的元素了
if (nextNode == null) throw new NoSuchElementException();
//否则,调用advance()方法,将返回当前的nextItem,同时计算下一次的nextNode、nextItem,本次的nextNode赋值给lastRet
return advance();
}
/**
* 移除元素
*/
public void remove() {
//获取最后返回的结点
Node<E> l = lastRet;
//如果为null,那么抛出IllegalStateException异常,表示没有可删除的结点
if (l == null) throw new IllegalStateException();
//这里的删除仅仅是将item置为null而已,并没有尝试移除结点,该结点将变成哨兵结点被后续的线程删除
l.item = null;
//lastRet置为null,表示最后返回的结点已删除
lastRet = null;
}
}
复制代码
3 ConcurrentLinkedQueue的总结
ConcurrentLinkedQueue是一个非阻塞式的同步队列,底层使用单向链表结构来保存队列元素,采用先进先出的数据访问方式。
所有的操作都没有加锁,而是针对head、tail、item、next提供了一系列的CAS方法,保证单个变量复合操作的原子性,同时上面的属性都是用volatile修饰,也保证了可见性和单个变量单操作的原子性,因此具有非常高的性能。
和传统队列不一样的是,外面持有的head、tail不一定是指向真正的头结点和尾节点,他们是弱一致性的,如果要保证强一致性那么需要每一个线程都在操作节点后都需要使用循环CAS更新head、tail的引用,在高并发环境下会因为很多线程空转而造成CPU的极大浪费,同时可能会造成总线风暴,并且也没必要做到head、tail的强一致性,因为外部人员根本不知道这两个变量,他们只需要offer、poll的结果是正确的就行了。
由于ConcurrentLinkedQueue支持所有操作并发,因此带有统计、比较性质的方法均不准确,比如size、isEmpty、iterator、contains等方法!
如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!