实现线程安全的队列有两种方式:一种基于阻塞的实现,用锁实现;另一种是基于非阻塞的实现,使用循环CAS实现。ConcurrentLinkedQueue就是基于非阻塞的方式实现的
一、属性变量
//队头 private transient volatile Node<E> head; //队尾 private transient volatile Node<E> tail;
二、内部节点类
private static class Node<E> { //节点中的值 volatile E item; //节点的后继 volatile Node<E> next; //使用CAS来构造节点 Node(E item) { UNSAFE.putObject(this, itemOffset, item); } //使用CAS更新节点的值 boolean casItem(E cmp, E val) { return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val); } //使用CAS节点val更新为cmp节点 boolean casNext(Node<E> cmp, Node<E> val) { return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val); } }
三、操作方法
1、入队方法
public boolean offer(E e) { //判断入队节点是否为空 checkNotNull(e); //构造入队节点 final Node<E> newNode = new Node<E>(e); //死循环,入队不成功继续入队 //p初始为tail for (Node<E> t = tail, p = t;;) { //q为p的后继,若p为队尾,则q为空,若p不是队尾,则有其他线程更改了队列 Node<E> q = p.next; if (q == null) { //p是队尾 将入队节点变成队尾的后继 if (p.casNext(null, newNode)) { //判断tail节点是否为尾节点 if (p != t) //如果不是,将入队节点修改为队尾 casTail(t, newNode); return true; } // Lost CAS race to another thread; re-read next } //说明这个节点已经被移除了 else if (p == q) //如果队尾未发生改变,t还是队尾,则将p指向t,继续插入新节点 //如果队尾发生改变,则将p指向队头重新开始插入新节点 p = (t != (t = tail)) ? t : head; else //说明p有后继,p的后继才是队尾,则需要将p指向队尾 p = (p != t && t != (t = tail)) ? t : q; } }
入队方法个人感觉还是挺复杂的。大致步骤如下
1、构造新节点
2、开始循环,p表示队尾,默认p初始为tail,
3、如果p后继为空,说明p就是tail,则插入新节点返回
4、如果p节点被删除,则需要重新赋值p,如果tail未变,则将p赋值为t,如果tail改变,则将p赋值为队头,继续插入
5、如果p有后继,说明p不是tail,则需要将p指向tail。
2出队方法
public E poll() { restartFromHead: //死循环 for (;;) { for (Node<E> h = head, p = h, q;;) { //取出队头的值 E item = p.item; //如果队头值不为空则将队头的值更新为空 if (item != null && p.casItem(item, null)) { //判断p是否等于head,如果不等于则重新指向队头 if (p != h) updateHead(h, ((q = p.next) != null) ? q : p); return item; } //队列为空 else if ((q = p.next) == null) { updateHead(h, p); return null; } //有其他线程将队头的值出队了,则重新开始出队 else if (p == q) continue restartFromHead; else p = q; } } }
出队的大致步骤如下:
1、h等于head,p初始化为head
2、取出队头的值
3、如果队头的值不为空,则将队头的值置空,然后判断head是否变化,如果变化重新寻找head,返回队头的值
4、如果队列为空,则需要重新寻找队头,返回null
5、如果有其他线程将队头的值出队了,那么重新从最新head开始出队
在最后,不得不说,这个源码是真的难以看懂。