目录
1 队列概述
线程安全的并发队列主要有两个,ConcurrentLinkedQueue和ConcurrentLinkedDeque。区别是前者是单向队列,只能FIFO,后者是双向队列,可以FIFO也可以FILO
这里先说一下队列的常用方法
add:将指定的元素插入到此队列中(如果立即可行且不会违反容量限制),在成功时返回 true,如果当前没有可用空间,则抛出 IllegalStateException。
offer:将指定元素插入到此队列的尾部(如果立即可行且不会超出此队列的容量),在成功时返回 true,如果此队列已满,则返回 false。
put:将指定元素插入到此队列的尾部,如有必要,则等待空间变得可用。
remove:移除并返回元素,若队列为空,则抛异常。
poll:移除并返回元素,若队列为空,返回null。
take:若队列为空,发生阻塞,等待有元素。
element:返回队列头部的元素, 如果队列为空,则抛出一个NoSuchElementException异常
peek:返回队列头部的元素,如果队列为空,则返回null
Concurrent系列的队列由于不是阻塞队列,所以没有实现put、take方法。并且该队列是基于链表实现的无界队列,理论上不会说没有空间。所以add和offer方法是一样的,不会抛异常
2 ConcurrentLinkedQueue
2.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) {
// 如果q为null,则p是尾节点,cas方式把新的节点追加到p的后面
if (p.casNext(null, newNode)) {
if (p != t)
//如果p不等于t,说明已经走过了(p = (p != t && t != (t = tail)) ? t : q;)这一步,这里需要重新设置一下尾节点
casTail(t, newNode);
return true;
}
}
else if (p == q)
// 在更新头结点(updateHead)的时候会出现头结点自引用的情况
p = (t != (t = tail)) ? t : head;
else
//p有next节点,表示p的next节点是尾节点,则需要重新更新p后将它指向next节点
p = (p != t && t != (t = tail)) ? t : q;
}
}
一般情况下队列设计的时候尾节点实时指向最新添加节点,但是针对ConcurrentLinkedQueue是每添加两个元素维护把tail节点指向一次尾节点,目的是减少casTail(t, newNode)操作,提高性能,具体流程如图
2.2 poll方法
public E poll() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
// 如果item不为空则把head的item掷空
if (item != null && p.casItem(item, null)) {
if (p != h)
// 如果p不是head节点则说明已经走过了p = q步骤,进入内循环的第二次循环,需要更新头结点
updateHead(h, ((q = p.next) != null) ? q : p);
// 直接返回item元素
return item;
}
else if ((q = p.next) == null) {
// 如果p节点的next为null,则说明p节点是头结点,更新头结点为p
updateHead(h, p);
return null;
}
else if (p == q)
continue restartFromHead;
else
// 如果item == null则把p指向下一个节点重新开始
p = q;
}
}
}
final void updateHead(Node<E> h, Node<E> p) {
if (h != p && casHead(h, p))
// 将旧的头结点的next指向头结点
h.lazySetNext(h);
}
一般情况下队列设计的时候头节点实时指向第一个节点,但是针对ConcurrentLinkedQueue是每移除两个元素维护把head节点指向一次第一个元素,目的是减少updateHead(t, newNode)操作,提高性能,具体流程如图
2.3 remove方法
public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
remove方法比较简单,这里直接用的是AbstractQueue中的方法
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) {
// 如果item不为空或者头结点的后继节点为空则表示p是第一个节点,更新为头结点并返回元素
updateHead(h, p);
return item;
}
else if (p == q)
// 如果p
continue restartFromHead;
else
// 如果head节点为空则向后走
p = q;
}
}
}
peek方法比较简单,就是获取第一个元素,同时更新下头结点
2.5 element方法
public E element() {
E x = peek();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
element方法比较简单,这里直接用的是AbstractQueue中的方法
3 ConcurrentLinkedDeque
ConcurrentLinkedDeque队列是双向队列,在原有队列的基础上增加了如下方法,感兴趣的同学可以读一下源码,
和ConcurrentLinkedQueue类似,笔者就不再详细记录了
pollFirst
pollLast
removeFirst
removeLast
offerFirst
offerLast
addFirst
addLast
4 debug源码的坑
有兴趣的同学可以debug下源码,看一下offer和poll的具体执行过程,便于更好的理解源码。坑爹的是idea调试的时候有个坑,第一次offer会把head指向新节点,tail节点自引用,导致我一度怀疑我的代码认知,也是折腾了一天才搞好,调试的时候可以参考如下文章配置一下idea:debug ConcurrenLinkedQueue的坑