今天在看ConcurrentLinkedQueue的1.8源码,结合网上的一些博客,发现有一些地方有坑,不是太好理解,就写下来总结一下吧。
首先,ConcurrentLinkedQueue是一个并发容器,它的并发性是通过CAS来实现的,这是实现非阻塞并发算法的基础。然后,head/tail 并非总是指向队列的头 / 尾节点,也就是说允许队列处于不一致状态,这个特性把入队 / 出队时,原本需要一起原子化执行的两个步骤分离开来,从而缩小了入队 / 出队时需要原子化更新值的范围到唯一变量,这是非阻塞算法得以实现的关键。另外,下面的两点,很重要,这是理解算法的关键:
head的不变性和可变性条件
不变性:
1. 所有未删除节点,都能从head通过调用succ()方法遍历可达。
2. head不能为null。
3. head节点的next域不能引用到自身。
可变性:
1. head节点的item域可能为null,也可能不为null。
2. 允许tail滞后(lag behind)于head,也就是说:从head开始遍历队列,不一定能到达tail。
tail的不变性和可变性条件
不变性:
1. 通过tail调用succ()方法,最后节点总是可达的。
2. tail不能为null。
可变性:
1. tail节点的item域可能为null,也可能不为 null。
2. 允许tail滞后于head,也就是说:从head开始遍历队列,不一定能到达tail。
3. tail节点的next域可以引用到自身。
下面来看入队操作:
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)
{
if (p.casNext(null, newNode))
{
if (p != t) //1
casTail(t, newNode);
return true;
}
}
else if (p == q) //2
p = (t != (t = tail)) ? t : head;
else //3
p = (p != t && t != (t = tail)) ? t : q;
}
}
在这我捡几个疑惑说:
1. if (p != t) 在单线程中是不需要考虑这个问题的,这个if判断一直为假。那么,在多线程中,这个判断什么时候为真呢?答案是在下面的else if,p可能会赋值为head,是不是会感到奇怪,在2中我会解释。p != t,说明此时tail没有更新,我们用casTail方法进行更新。在这里,casTail即使失败也没有关系,因为这说明有其他线程对tail更新了。
2. if (p == q) 什么情况下会出现,上面我们在tail的可变性上说了,tail.next可以指向自己,这表明tail现在所在指向的结点已被删除(从head遍历无法到达tail),那么就要从head开始遍历到所有的未删除结点。这就是我们要将p赋值为head的原因。
3. 剩下这种情况就是tail不是指向最后一个节点的时候,此时我们将p更新为q。
再来看出队操作:
public E poll()
{
restartFromHead:
for (;;)
{
for (Node<E> h = head, p = h, q;;)
{
E item = p.item;
if (item != null && p.casItem(item, null))
{
if (p != h) //1
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
else if ((q = p.next) == null) //2
{
updateHead(h, p);
return null;
}
else if (p == q) //3
continue restartFromHead;
else //4
p = q;
}
}
}
看明白上面offer的几个疑点,相信这就不难解释了
1. 这和offer的1很相似,这时我们更新head节点。
2. head的item为null,而它next节点为null的时候,将head指向p这个伪节点,返回null。
3. p == q这个条件有点奇怪,应该是在4时候将q赋值给p,循环后另一个线程将q这个节点给删掉了,此时进入3
4. 在结点出队失败后可以保证下次尝试出队时p不为空(之前q = p.next != null才有可能跳到这一步)