首先看一下ConcurrentLinkedQueue继承关系
从这里可以看到ConcurrentLinkedQueue没有实现BlockingQueue接口,所以是非阻塞的队列
ConcurrentLinkedQueue简介
ConcurrentLinkedQueue是基于链表的非阻塞无界队列,采用先进先出(FIFO)的规则对节点进行排序,使用CAS非阻塞算法来保证多线程下入队出队操作的线程安全(又名wait-free算法),是线程安全的,底层是使用单向链表实现的,类似于LinkedBlockingQueue中的链表结构
ConcurrentLinkedQueue主要成员变量
同样的类似于LinkedBlockingQueue,每个元素封装成一个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节点
Node(E item) {
UNSAFE.putObject(this, itemOffset, item);
}
// 修改节点的item
boolean casItem(E cmp, E val) {
return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
}
// 懒修改节点的next
void lazySetNext(Node<E> val) {
UNSAFE.putOrderedObject(this, nextOffset, val);
}
// CAS修改节点的next节点
boolean casNext(Node<E> cmp, Node<E> val) {
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
// Unsafe mechanics
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);
}
}
}
因为每个节点会将元素封装成一个Node,将元素保存在item中,将下一个元素的引用保存在next中,从而实现链表结构,元素按照 FIFO 的形式来访问,队列头部为待的时间最久的元素,尾部则是最少,新元素插在尾部。
不同于LinkedBlockingQueue中对Node的操作,如果需要对Node进行操作,需要通过CAS方式进行修改,不能直接进行修改,否则不能保证线程安全性
ConcurrentLinkedQueue源码分析
ConcurrentLinkedQueue主要成员变量
// 头节点
private transient volatile Node<E> head;
// 尾节点
private transient volatile Node<E> tail;
这里先简单的说一下head指向的不一定是最前面的节点,tail也不一定是最末尾的节点
ConcurrentLinkedQueue构造方法
public ConcurrentLinkedQueue() {
head = tail = new Node<E>(null);
}
初始化的时候会首先创建一个空节点,从这个空节点开始进行入队和出队
ConcurrentLinkedQueue添加元素
ConcurrentLinkedQueue–offer
add实际上调用子类的offer方法添加数据,因为继承了AbstractQueue抽象类
public boolean offer(E e) {
// 判断是否为空元素
checkNotNull(e);
// 将添加元素封装成Node节点
final Node<E> newNode = new Node<E>(e);
// while循环,获取tail并赋给局部变量t,同时将t赋给p
for (Node<E> t = tail, p = t;;) {
// 获取p的next节点
Node<E> q = p.next;
// 判断下一个节点是否为空
if (q == null) {
// p is last node
// 为空,说明p后面没有元素了,说明p是最后一个节点
// 通过cas方式设置p的next为新添加的元素
if (p.casNext(null, newNode)) {
// Successful CAS is the linearization point
// for e to become an element of this queue,
// and for newNode to become "live".
// 判断p是否和t相等
if (p != t) // hop two nodes at a time
// 如果不相等,重新将tail设置成最新的节点
casTail(t, newNode); // Failure is OK.
// 返回
return true;
}
// Lost CAS race to another thread; re-read next
}
// 如果p和q相等
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.
// 判断此刻的tail是否和刚添加元素的时候的t相等,如果不相等,将t赋给p,如果相等将head赋给p
p = (t != (t = tail)) ? t : head;
else
// Check for tail updates after two hops.
// 判断t和p是否相等,如果不相等,说明p移动了,如果相等说明t还能用
// 判断tail和添加元素时候的t是否相等,如果不相等,说明tail让其他线程改变了,重新定位p的位置为q
p = (p != t && t != (t = tail)) ? t : q;
}
}
其实干说起来上面的代码还是比较蒙的,接下来,通过图片一一讲解一下
首先初始化的时候
当第一个线程添加元素的时候,while循环开始初始化
第一添加完元素的状态,当再次添加元素的时候,tail的位置没有改变,所以此时获取的位置还是初始的位置,此时在获得p的next的时候就不为空,next为刚才新添加的节点
所以进入最后一个else,也就是p = (p != t && t != (t = tail)) ? t : q;
首先判断p是否等于t,由于此时只有一个线程,所以p和t是相等的,p没有移动,所以此时会返回false,会将q赋给p
此时进入第二遍循环的时候,p指向刚才添加的节点,q指向null,此时进入第一if判断,通过cas设置p的next指向新添加的节点
此时的链表结构如上,通过cas方式设置了p的后继节点,然后判断此时的p和t是否相等,发现现在p和t是不相等的,所以需要更新tail为最新的节点,通过cas方式设置newNode1为tail节点,返回true
以上方法的分析只是线程顺序执行的效果,在并发情况下,再次分析一下
假如线程1和线程2同时获取链表执行初始状态,线程1向链表中添加了两个元素之后,线程2才开始添加元素
线程2在判断链表的时候发现线程1已经修改链表了
线程2判断的时候,发现q不为null,进入else,判断p和t是否相等,如果相等,则移动p的位置
再次循环,q依然不为空,进入else,判断p和t是否相等,如果不相等,进行t != (t = tail)的判断,其实这句话也好理解,就是获取当前最新的tail节点的位置,赋值给t,判断tail现在的位置是否和线程当初进行while循环时是否相等,如果不相等,说明tail的位置移动了,所以需要重新定位tail的位置为最新的位置,并赋值给t,同时重新定位p的位置为最新tail的位置
所以通过上面的分析,我们发现通过tail引用定位了线程2应该添加元素的位置
以上就是对于多线程下如何进行元素添加的分析,其实这里还有一个问题就是p==q,这种情况,想要知道这种情况,就需要知道出队的时候进行了什么操作,才能理解
ConcurrentLinkedQueue–poll
接下来我们看一下出队的方法poll
public E poll() {
restartFromHead:
// 外层while循环
for (;;) {
// 内层while循环,首先获取head节点,赋给h,同时将h赋给p,初始化q
for (Node<E> h = head, p = h, q;;) {
// 获取p保存的元素item
E item = p.item;
// 判断item是否为空,如果不为空的同时,设置p保存的item为null,如果都成功,说明已经成功清除了p上面保存的数据
if (item != null && p.casItem(item, null)) {
// Successful CAS is the linearization point
// for item to be removed from this queue.
// 判断p和h是否相等
if (p != h) // hop two nodes at a time
// 如果不相等,更新head,将p的后继节点赋给q,同时判断后继节点是否为空
// 如果不为空,说明p节点的后继节点可能是head节点
// 如果为空,说明p节点后面没有节点了,那么说明p就是head节点
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
// 如果发现p的后继节点为空,说明p是新的head
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
// 如果发现p的后继节点和p相等
else if (p == q)
// 重新进行while循环,获取最新的head后再取出数据
continue restartFromHead;
else
// 移动p,为后继节点
p = q;
}
}
}
再看一下updateHead都做了什么,通过方法名,知道是更新head节点
final void updateHead(Node<E> h, Node<E> p) {
// 判断h和p是否相等,不相等才有替换的必要
// 通过cas设置p
if (h != p && casHead(h, p))
// 如果成功,通过cas方式将h的next设置为自己
h.lazySetNext(h);
}
通过观察上面方法,我们知道了为什么有的时候需要判断p == q,因为在调用updateHead的时候会调用h.lazySetNext(h)方法,next引用自己,目的是将这个节点脱离链表,没有引用指向这个节点后,通过垃圾回收机制将这个对象清除掉,所以在发生p == q的时候需要重新定位tail和head的位置,保证获取到最新的节点信息
接下来看一下在单线程条件下,元素是如何出队的
进行里层while循环的时候状态如图所示,此时队列中没有数据,进入第二个if判断,p的后继节点是否为空,如果为空设置head为p,返回null后直接跳出循环
当队列中含有数据的时候,再次执行poll的时候,判断p中存储的元素是否为null,如果为空,进入第二个if判断,首先获取p的后继节点q,判断q是否为空,q不为空,所以再次进行第三个if判断,p和q是否相等,显然是不相等的,所以进入最后一个else,将q赋值给p
再次进入while循环的时候,此时获取p的元素,不为空,通过cas方式设置p保存的item为空,如果成功说明这个元素可以返回
再判断p和h是否相等,此时如果不相等,说明p的位置移动了,需要更新head节点的位置,此时判断p的后继节点是否为空,如果不为空,说明此时的节点应该脱离队列,p后面的后继节点应该为head节点,如果为空,说明p后面已经没有数据了,那么此时p就应该是head节点,将head设置为p,后返回item
当再次获取元素的时候
这是在单线程的条件下执行,我们理解了,当在多线程的情况下是如何运行的呢?
线程1和线程2同时获取数据,线程1获取完元素之后,更新head节点的位置,此时的线程2判断,p存储的元素是否为空,如果为空,则设置q等于p进行下一次循环
此时的线程2进行第二次循环的时候,获取的是线程1操作完的状态,线程2在进行第二次循环的时候,判断p保存的元素是否为空,此时判断元素为空,因为线程1在获取完元素会通过CAS把item设置为空,之后线程2再进行判断的时候判断为空
进入第二个if判断,将p的后置节点赋给q,判断q是否为空,因为p的后置节点指向自己,所以q不为空,这时在进入第三个if判断,此时判断p和q是否相等,因为q指向自己,所以此时p和q是相等的,跳出里层while循环,重新进行外层while循环,也就是重新获取head节点的位置,重新从最新的head节点开始进行获取元素
通过上面的分析,我们知道了,为什么会有p等于q的情况,实际上就是链表在丢弃数据的时候,为了将丢弃的数据进行垃圾回收,导致后置节点指向自己的情况发生,从而需要重新获取此时的head节点的最新位置,进行数据取出操作
我们现在再回到插入数据的时候,在插入数据的时候,从tail开始插入的时候,刚好判断p的后继节点q等于自己,说明此时的节点已经脱离链表了,需要重新定位此时p的位置
首先判断tail的位置是否和刚刚进入到while循环的时候的位置相等,如果相等,则定位p的位置为head的位置,如果不相等,则定位p的位置为tail最新的位置
此时判断p与q是相等的,并且tail的位置没有移动,说明整个这个节点都是不可用的,整个节点都从链表中脱离了,所以需要重新定位tail的位置,所以需要从head的位置重新定位p和tail的位置
同样的,此时p与q是相等的,并且tail的位置已经通过别的线程移动了,同样的此时这个节点也是不可用的,整个节点同样的需要从链表中脱离,但是发现此时的tail节点的位置改变了,所以应该使用最新的tail节点进行增加操作,所以从最新的tail开始,进行后续节点的更新
通过上面的分析,我们同时还可以得出tail有时候会滞后于head的结论,因为此时的tail节点已经脱离的队列,所以需要重新从head开始,重新进行节点位置的定位
ConcurrentLinkedQueue–remove
接下来再简单的看一下remove方法
public boolean remove(Object o) {
// 判断是否为空
if (o != null) {
// 初始化指针
Node<E> next, pred = null;
// 获取链表第一个节点,一直遍历到链表尾部,p为空为止
// 赋值临时变量p给pred,同时将next赋给p
for (Node<E> p = first(); p != null; pred = p, p = next) {
boolean removed = false;
// 获取p保存的元素
E item = p.item;
// 如果不为空
if (item != null) {
// 判断item与o是否相等
// 如果不相等
if (!o.equals(item)) {
// 获取p的下一个元素
next = succ(p);
// 继续下一次循环
continue;
}
// 如果相等,通过cas将p的item设置为null
removed = p.casItem(item, null);
}
// 同时获取p的下一个元素
next = succ(p);
// 如果p刚才保存的元素和next保存的元素都不为空
if (pred != null && next != null) // unlink
// 通过cas方式将pred的next设置为最新的next
pred.casNext(p, next);
// 如果cas成功,则返回移除成功
if (removed)
return true;
}
}
return false;
}
首先简单的看一下succ方法
final Node<E> succ(Node<E> p) {
Node<E> next = p.next;
return (p == next) ? head : next;
}
其实在了解元素出队列的特殊情况,再来看这段代码就非常简单了,首先获取p的后继节点,如果p的后继节点和p相等,说明这个元素已经脱离队列了,所以返回head节点,如果不相等,说明这个元素还在链表中,返回p的后继节点
之后再看一下first方法
Node<E> first() {
restartFromHead:
// 外层while循环
for (;;) {
// 获取head节点,初始化临时变量h,p,q
for (Node<E> h = head, p = h, q;;) {
// 判断p保存的元素是否为空,如果不为空
// 判断p的后继节点是否为空
// 如果上面两个条件有一个成立,则更新head为p节点
boolean hasItem = (p.item != null);
if (hasItem || (q = p.next) == null) {
updateHead(h, p);
// 如果有元素则返回p
// 如果没有元素,则返回null
return hasItem ? p : null;
}
// 如果判断p等于q,说明脱离链表了,重新定位元素位置
else if (p == q)
continue restartFromHead;
else
// 其他情况定位p为下一个节点,进行下一次循环
p = q;
}
}
}
通过上面出队代码的分析,我们知道队列可能会有两种情况
head有可能是哨兵节点,也有可能是保存元素的节点
ConcurrentLinkedQueue–size
public int size() {
int count = 0;
// 从head开始遍历
for (Node<E> p = first(); p != null; p = succ(p))
// 判断p保存的元素是否为空
if (p.item != null)
// Collection.size() spec says to max out
// 如果不为空,将count数量加一
// 如果等于Integer.MAX_VALUE则跳出循环,返回count
if (++count == Integer.MAX_VALUE)
break;
return count;
}
size没有加锁,就是遍历了整个队列,但是遍历的同时可能在发生poll或者offer,所以size不是特别的精确,用的时候要注意
ConcurrentLinkedQueue–总结
- ConcurrentLinkedQueue通过CAS保证链表的线程的安全
- ConcurrentLinkedQueue具有可变性和不可变性
- 不变形:head和tail都可以通过succ方法访问到;head != null;tail != null
- 可变性:head.item可能是null,也可能不是null;tail.item可能是null,也可能不是null;允许tail滞后于head;tail.next有可能指向tail
- ConcurrentLinkedQueue允许队列处于不一致状态,分离了poll和offer两个操作
- ConcurrentLinkedQueue与LinkedBlockingQueue对比,LinkedBlockingQueue能通过count来获取队列中元素准确的数量,因为添加和取出的时候都会对count进行修改,而且整个操作是原子性的,所以可以通过count返回队列中元素的个数,但是ConcurrentLinkedQueue对队列进行修改的时候不能保证原子性,所以导致不能对count进行修改,只能通过一种全部遍历的方式获取不准确的数值