概述
ConcurrentLinkedQueue是一个无边界的线程安全的非阻塞队列,遵循FIFO。队列的head节点是时间最长的节点,队列的tail节点是时间最短的节点。新元素将会插入在队列的tail节点之后,而队列的检索操作从队列的head节点开始。像很多并发集合一样,该队列不允许null元素。
该队列的迭代器iterator是弱一致性的,迭代器返回的元素只是反映了队列在迭代器创建时的状态。它们不会抛出ConcurrentModificationException,可以和poll、offer等其他操作同时进行。
happen-before原则:队列的元素插入操作优先于队列的元素访问和删除操作。
构造函数
/**
* Creates a {@code ConcurrentLinkedQueue}
* initially containing the elements of the given collection,
* added in traversal order of the collection's iterator.
*
* @param c the collection of elements to initially contain
* @throws NullPointerException if the specified collection or any
* of its elements are null
*/
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;
}
offer()方法
tail节点并不一定是指向队列的最后一个节点,它可能指向最后一个节点的前一个节点。
为了减少更新tail节点的次数,提高入队的效率,Doug Lea并没有让tail节点作为队尾last节点,只有tail节点与last节点之间的距离等于1的时候才需要更新tail节点。即队列每追加2个节点才会更新一次tail指针。
假设最初状态是tail节点等于last节点,则追加的情形如下:
- 追加第一个节点时,tail节点的next指针为null。走{语句2},更新队列的tail节点的next节点为新节点。但不走{语句3}。
- 追加第二个节点时,tail节点的next指针不为null。走{语句4}和{语句1},连跳2个节点,然后走{语句3}更新tail节点。此时又回到最初状态 —— tail节点等于last节点。
/**
* Inserts the specified element at the tail of this queue.
* As the queue is unbounded, this method will never return {@code false}.
*
* @return {@code true} (as specified by {@link Queue#offer})
* @throws NullPointerException if the specified element is null
*/
public boolean offer(E e) {
checkNotNull(e);
final Node<E> newNode = new Node<E>(e);
for (Node<E> t = tail, p = t;;) { //循环初始化p为tail节点
Node<E> q = p.next; //语句1处
if (q == null) {
// p is last node
//如果q等于null,说明tail节点p是队尾节点
if (p.casNext(null, newNode)) { //语句2处
// Successful CAS is the linearization point
// for e to become an element of this queue,
// and for newNode to become "live".
//语句4处设置p=q后则满足条件p!=t
if (p != t) // hop two nodes at a time 追加2个节点才更新tail指针
casTail(t, newNode); // Failure is OK. //语句3处
return true;
}
// Lost CAS race to another thread; re-read next
}
//ConcurrentLinkedQueue允许list的并发修改,当一个线程在迭代这个节点,
//可能另一个节点在删除这个节点,updateHead()方法会将已删除节点的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节点没改变,跳到head节点,否则跳到新的tail节点
p = (t != (t = tail)) ? t : head;
else
// Check for tail updates after two hops.
p = (p != t && t != (t = tail)) ? t : q; //语句4处
}
}
poll()方法
head节点并不一定是指向队列的第一个有效节点(有效是指节点的item不为null),它可能指向第一个有效节点的前一个无效节点。
为了减少更新head节点的次数,提高出队的效率,Doug Lea让队列每出队2个节点才会更新一次head指针。
假设最初状态是head节点等于队首节点,则出队的情形如下:
- 出队第一个节点。走{语句1},将head节点的item字段通过cas设置为null。不更新head指针为next节点。
- 出队第二个节点。走{语句1}发现head节点的item等于null,属于无效节点。走{语句3}将q节点设置为head节点的后继节点,q节点此时即第二个节点。q节点不为null,走{语句4}将p设置为q节点。再走{语句1},将第二个节点的item字段通过cas设置为null。最后走{语句4},更新head指针,设置head指针为第二个节点的next节点,从而连跳2个节点。
public E poll() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
//将head节点的item字段通过cas设置为null
if (item != null && p.casItem(item, null)) { //语句1
// Successful CAS is the linearization point
// for item to be removed from this queue.
if (p != h) // hop two nodes at a time
//如果p节点不等于head节点(一般此时因为走{语句3}和{语句4},p节点等于head节点的后继节点,所以不等)
//并且如果p节点的next指针不为null,通过cas设置head指针为p节点的next节点。
//即此时head指针连跳2个节点
updateHead(h, ((q = p.next) != null) ? q : p); //语句2
return item;
}
//将q节点设置为head节点的后继节点,并判断该节点是否为null
else if ((q = p.next) == null) { //语句3
updateHead(h, p);
return null;
}
else if (p == q)
continue restartFromHead;
else
//将p设置为q节点
p = q; //语句4
}
}
}
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) {
UNSAFE.putOrderedObject(this, nextOffset, val);
}
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);
}
}
}
参考:java并发之ConcurrentLinkedQueue
Simple,fast,practical non-blocking and blocking concurrent-queue algorithms
摘要
我们设计了一个新的非阻塞并发队列算法,和只有2个锁的阻塞队列算法(使入队和出队可以同时进行)。2个算法都是Simple,fast,practical的。
介绍
并发的FIFO队列广泛应用于各种应用和系统当中。为了保证正确性,队列的并发访问必须保证同步。通常来讲,并发数据结构的算法,包括FIFO队列,分为2种:非阻塞和阻塞。在多核环境中,阻塞算法的性能下降更严重,尤其是操作在某个点被挂住或者延迟时。造成延迟的原因可能是处理器调度抢占、页错误、缓存丢失。非阻塞算法在这些面前表现得更加强大。
很多的非阻塞算法都是基于compare_and_swap,因此必须处理ABA问题:
- 线程 1 从内存位置V中取出A。
- 线程 2 从位置V中取出A。
- 线程 2 进行了一些操作,将B写入位置V。
- 线程 2 将A再次写入位置V。
- 线程 1 进行CAS操作,发现位置V中仍然是A,操作成功。
算法
图一展示了非阻塞队列的数据结构和操作的伪代码。该算法中的非阻塞队列是一个带有head和tail指针的linkedList。该算法使用compare_and_swap,通过使用计数器counter避免ABA问题。为了允许出队操作可以释放出队节点,出队操作确保tail指针不会指向出队节点和任意前继节点,这意味着出队节点可以安全地被复用。
图一展示了只有2个锁的阻塞队列的数据结构和操作的伪代码。该算法使用2个独立的head锁和tail锁,从而允许入队和出队可以并发进行。跟在非阻塞队列一样,我们使用了一个冗余的节点放在linkedList的开始处。因为这个冗余节点,入队不需要去访问head节点(只访问tail节点),出队不需要去访问head节点(只访问head节点),这样避免了可能的死锁问题。
译自:Simple,fast,practical non-blocking and blocking concurrent-queue algorithms