【J.U.C-Collections】并发集合类与实现原理—— ConcurrentLinkedQueue

本文主要分析非阻塞同步队列ConCurrentLinkedQueue,原理基于volatile修饰的Node节点+自旋循环CAS。

其实现同步入队出队,是通过volatile类型head、tail指针的位置(本身或next节点是否为空)判断是否有别的线程先入队出队成功了。

注意头尾节点的更新是延迟的,如入队操作是每两次才定位校正一次尾结点,因为这样可以把一次volatile写操作替换为volatile读,提高效率。

Queue与BlockingQueue

Queue队列:FIFO先进先出,元素被添加到队列末尾,元素从队列头取出;每一个Queue对于元素的插入删除等操作都有两种形式的实现:

操作抛出异常返回特数值
插入add(e)offer(e)
删除remove()poll()
检查element()peek()

对于队列有两种实现方式:

  • 非阻塞队列
    我们常用的LinkedList就是非阻塞队列,包括线程安全的 ConcurrentLinkedQueue
  • BlockingQueue阻塞队列
    当队列为空时,取元素的操作将被阻塞;当队列为满时,添加元素的操作将被阻塞;
    也就是说,在不满足条件的情况下,当前线程会被阻塞,直到满足条件时会被 自动唤起

BlockingQueue的实现类:

  1. ArrayBlockingQueue :一个由 数组 结构组成的有界阻塞队列
  2. LinkedBlockingQueue :一个由 链表 结构组成的有界阻塞队列
  3. PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列
  4. DelayQueue:一个使用优先级队列实现的无界阻塞队列
  5. SynchronousQueue:一个不存储元素的阻塞队列
  6. LinkedTransferQueue:一个由链表结构组成的无界阻塞队列(实现了继承于 BlockingQueue 的 TransferQueue)
  7. LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列

相比于Queue,阻塞队列的接口的实现有四种形式

操作抛出异常返回特数值一直阻塞超时退出
插入add(e)offer(e)put(e)offer(e,time,unit)
删除remove()polltake()poll(time,unit)
检查element()peek--

本文主要分析线程安全的非阻塞队列实现方式。
在线程安全的队列中,非阻塞队列是通过CAS实现,阻塞队列是通过加锁实现的

非阻塞队列——ConcurrentLinkedQueue【线程安全】

1. 存储结构与构造函数

存储结构

public class ConcurrentLinkedQueue<E> extends AbstractQueue<E>
        implements Queue<E>, java.io.Serializable {
		
		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);
            }
        }
    }

private transient volatile Node<E> head;

private transient volatile Node<E> tail;
  • 队列存储的结构为Node节点;
  • Node节点中的两个属性item和next都是 Volatile 类型,保证可见;
  • Node结构中,是通过 Unsafe类的CAS 操作来实现赋值等操作的。

构造函数

public ConcurrentLinkedQueue() {
        head = tail = new Node<E>(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;
    }
  • 如果不指定大小,那么初始化head和tail都为一个空节点;
  • 如果有一个已有的集合,那么遍历集合元素创建新节点,head与tail都指向第一个节点,遍历过程中将tail的next不断指向新Node,且tail后移,直到最后一个元素。

2.【ConcurrentLinkedQueue功能实现】——入队

public boolean add(E e) {
        return offer(e);
    }
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) {
                // 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".
                    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
            }
            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
                // Check for tail updates after two hops.
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

  1. 首先看for循环的条件:for (Node t = tail, p = t; ; )
    表示自旋循环,直到插入元素成功才return true退出。
指针t、p
队列元素NodeNodeNode
  1. 线程A有插入的操作,要插入元素Ae,进入第一个if条件:if (q == null)
    这种是没有其他线程干扰的情况,尝试在下一个节点通过 CAS插入 新元素:p.casNext(null, newNode),插入成功,返回true;
    操作 | 抛出异常 | 返回特数值 | 一直阻塞 |超时退出
指针t、pq
队列元素NodeNodeNodeNode(Ae)
  1. 如果有另外一个线程B此时也正在插入操作,也进入了q==null这个条件中,那么 p.casNext失败
  • 线程B进入下一次自旋;
  • 这时q是A插入的Ae,不为空,线程B下一次循环会进入到最后一个条件:
  • p = (p != t && t != (t = tail)) ? t : q;这行代码的作用是,将p指向真正的队尾,也就是q=Ae的位置。
指针tq、p
队列元素NodeNodeNodeNode(Ae)
  • 线程B进入下一次自旋,q=p.next又变成了null;
指针tpq
队列元素NodeNodeNodeNode(Ae)null
  • 进入q==null的条件中,尝试p.casNext操作,成功;
指针tpq
队列元素NodeNodeNodeNode(Ae)Node(Be)
  • 此时p!=t,通过casTail将t指向最新的节点Be,返回true。
指针pq、t
队列元素NodeNodeNodeNode(Ae)Node(Be)
  • 总结来看大体逻辑就是:
    标记p为当前tail节点位置,p.next节点为空时,CAS新节点到p.next的位置;
    如果失败就循环重试;
    在这个过程中有其他节点先插入成功了,那么先将p校正到真正的尾结点,再重试;
    重试成功之后再校正真正的tail节点位置。
  • 可以看出,tail并不是一直指向真正的尾结点,每两次CAS新节点成功时才更新一次tail

3.【ConcurrentLinkedQueue功能实现】——出队

public E poll() {
        restartFromHead:
        for (;;) {
            for (Node<E> h = head, p = h, q;;) {
                E item = p.item;

                if (item != null && p.casItem(item, null)) {
                    // Successful CAS is the linearization point
                    // for item to be removed from this queue.
                    if (p != h) // hop two nodes at a time
                        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. 线程A有出队操作,初始时Node h = head, p = h,p.item为空
指针h、pq
队列元素nullNodeNodeNode
  1. 进入到最后一个分支:p=q
指针hq、p
队列元素nullNodeNodeNode
  1. 线程A进入下一次自旋,此时p.item != null,尝试p.casItem通过 CAS删除 第一个节点值,删除成功;
    进入到updateHead(h, ((q = p.next) != null) ? q : p),将头元素移除,重置h指针
指针pq、h
队列元素nullnullNodeNode
  1. 线程A继续删除元素,进入for循环,此时p = h;
指针h、p
队列元素nullnullNodeNode
  1. p.item!=null,尝试p.casItem删除节点值,删除成功;
    与上次不同的是p=h,不再挪动h指针。
指针h、p
队列元素nullnullnullNode
  • 总结来看大体逻辑就是:
    标记p为当前head的位置,当p值为空时,将其指向真正的第一个有效节点;
    CAS将p的位置赋值为null,如果失败就循环重试;
    在这个过程中有其他线程先出队成功了,那么先校正p到真正的第一个有效节点,再重试;
    重试成功之后再校验真正的head节点位置。
  • 可以看出,head并不一定一直指向第一个有效的节点,有可能是第一个有效节点的前一个节点;
  • 与入队的实现相似,都是两次出队才CAS更新一次head节点。

ConcurrentLinkedQueue总结

Volatile+循环CAS

  • 非阻塞队列通过 循环和CAS 来保证一致性,实现线程安全;

Hops延迟更新策略

  • 由入队与出队的逻辑,我们知道tail与head的指向并不是每一次都实时更新的,而是与真实的位置大于1时,完成入队或出队操作之后才会CAS更新一次。

  • Q:为什么不每一次CAS成功时都重置一下tail?比如入队操作时也就变成了
    q == null条件下,p.casNext(null, newNode) && casTail(t, newNode)
    A:重置tail需要进行一次CAS的写操作,取代写操作的是,每次入队都需要定位一次真正的尾结点, 用volatile读代替一次写 ,提高效率。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值