简介
ConcurrentLinkedQueue是JUC包下的线程安全的无界非阻塞队列,它与BlockingQueue接口实现类最大的不同就是,BlockingQueue是阻塞队列,而ConcurrentLinkedQueue是非阻塞队列。这里的阻塞非阻塞,指的是队列满了或为空的时候,线程移除或放入元素的时候,是否需要阻塞挂起。BlockingQueue底层是用锁实现的,而ConcurrentLinkedQueue底层使用CAS实现的。
实现原理
先来看重要的属性和数据结构:
// 头结点
private transient volatile Node<E> head;
// 尾结点
private transient volatile Node<E> tail;
// 初始化头、尾节点为哑节点
public ConcurrentLinkedQueue() {
head = tail = new Node<E>(null);
}
其中,Node是它的内部类:
private static class Node<E> {
volatile E item;
volatile Node<E> next;
Node(E item) {
UNSAFE.putObject(this, itemOffset, item);
}
// CAS方式修改当前结点的元素
boolean casItem(E cmp, E val) {
return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
}
// 延迟设置当前结点的后继结点
void lazySetNext(Node<E> val) {
// 有序、延迟版本的putObjectVolatile方法,不保证值的改变被其他线程立即可见。只有在field被volatile修饰时有效
UNSAFE.putOrderedObject(this, nextOffset, val);
}
// CAS方式设置当前结点的后继结点
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);
}
}
}
不难看出,ConcurrentLinkedQueue是用单向链表实现的。
再来看它的重要方法实现:
offer操作
public boolean offer(E 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 is last node
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".
// CAS操作成功
// 每隔1个节点,更新tail节点为新插入的节点,包含哑节点在内,第1,3,5,7..个节点设置为尾节点,也就是说这里的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
}
// p == q成立的场景在于,多线程环境下,其他线程调用了poll移除头部元素的操作,使得head自引用,即head.next==head,这时需要重新找新的head。可能还是不好理解,结合下面的poll()方法注释就清楚了
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
// 入队操作被其他线程抢先了,需要让p定位最新的尾节点,再去循环尝试CAS操作,直到成功为止。
// Check for tail updates after two hops.
// 这里的t != (t = tail)意思是检测tail是否发生变化,上面提到tail指向的是1,3,5,7...号节点,如果tail发生了变化,则更新t的值。
p = (p != t && t != (t = tail)) ? t : q;
}
}
offer()方法总结:
offer的关键步骤是用CAS操作尝试将newNode插入到队列尾部,如果CAS失败了,则定位到最新的尾结点,并循环重试,直到CAS成功。在多线程环境下,采用自璇的方式替代了锁阻塞方式,因此性能会提升,但也消耗了CPU资源。
再看poll操作:
public E poll() {
// goto标记
restartFromHead:
// 无限循环
for (;;) {
// 指针p从头结点开始
for (Node<E> h = head, p = h, q;;) {
// 保存当前元素
E item = p.item;
// 当前指向的结点元素不为空,则CAS方式尝试将当前结点元素置为null
if (item != null && p.casItem(item, null)) {
// Successful CAS is the linearization point
// for item to be removed from this queue.
// CAS操作成功,并且每隔1个节点,更新头结点,即头结点指向的是哑节点或者第一个元素结点。与offer()更新尾结点有点类似。
if (p != h) // hop two nodes at a time
updateHead(h, ((q = p.next) != null) ? q : p);
// 返回被移除的当前元素
return item;
}
// 当前队列为空,则返回null
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
// 如果当前结点自引用了,则重新从最新的头结点开始下一轮尝试
else if (p == q)
continue restartFromHead;
// 将p移动到下一个结点,目的是找到第一个元素不为null的节点,然后进行CAS尝试
else p = q;
}
}
}
其中updateHead代码如下:
final void updateHead(Node<E> h, Node<E> p) {
if (h != p && casHead(h, p))
// 更新头结点成功,则把旧的节点自引用,即h.next = h
// 目的是把它从链表中脱离出来,达到移除元素的效果,它最终会被垃圾回收。
h.lazySetNext(h);
}
poll()方法总结:
poll方法在移除头部元素时,是通过CAS操作将当前结点的元素值置为null,然后重新设置头结点,来达到移除元素的目的,被移除的结点会自引用,成为孤儿结点被垃圾回收掉。另外,如果在循环尝试过程中发现头结点发生了变化,会调到外层循环restartFromHead,从最新的head重新开始。
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;
}
}
}
可以看到,peek方法结构和poll差不多,只不过没有移除元素的操作,这也是合理的,因为peek操作只是查看队列头部元素。
size()方法:
public int size() {
int count = 0;
for (Node<E> p = first(); p != null; p = succ(p))
if (p.item != null)
// Collection.size() spec says to max out
if (++count == Integer.MAX_VALUE)
break;
return count;
}
要注意的是,这个类的size()方法不像大多数集合那样,它的size不是强一致性的,因为在调用size()方法时,在返回count前,可能有其他线程调用了offer或者poll方法,所以在多线程环境下,size()方法返回的队列长度并不总是精确,我们在使用时需要注意这点。
first()方法:
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;
}
}
}
注意,first()与peek()方法返回值不同,first()返回的是结点Node<E>,而peek()返回的是元素E。first()返回的是第一个元素结点,哑节点不算在内,即如果队列为空则返回null。并且,first()不是public的,默认是包内访问权限,它通常在类的内部其他方法调用,我们应用程序无法直接调用。
remove(Object o)方法:
public boolean remove(Object o) {
// o为null时直接跳到最后return false。
if (o != null) {
// 记录当前遍历指针p的后继结点next和前继结点pred,移除时会用到
Node<E> next, pred = null;
for (Node<E> p = first(); p != null; pred = p, p = next) {
boolean removed = false;
E item = p.item;
if (item != null) {
// 从头结点开始找链表中的目标元素
if (!o.equals(item)) {
next = succ(p);
continue;
}
// 找到目标元素,尝试CAS操作将元素置为null
removed = p.casItem(item, null);
}
next = succ(p);
// 将目标元素的前继结点和后继结点用CAS方式连接起来,达到移除目标元素的目的
if (pred != null && next != null) // unlink
pred.casNext(p, next);
// 移除成功返回true,否则继续往后遍历链表尝试移除目标元素
if (removed)
return true;
}
}
return false;
}
参考资料:
《Java并发编程之美》
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。