白话JUC--Queue体系--ConcurrentLinkedQueue

首先看一下ConcurrentLinkedQueue继承关系
在这里插入图片描述
从这里可以看到ConcurrentLinkedQueue没有实现BlockingQueue接口,所以是非阻塞的队列

ConcurrentLinkedQueue简介

ConcurrentLinkedQueue是基于链表的非阻塞无界队列,采用先进先出(FIFO)的规则对节点进行排序,使用CAS非阻塞算法来保证多线程下入队出队操作的线程安全(又名wait-free算法),是线程安全的,底层是使用单向链表实现的,类似于LinkedBlockingQueue中的链表结构

ConcurrentLinkedQueue主要成员变量

同样的类似于LinkedBlockingQueue,每个元素封装成一个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节点
        Node(E item) {
            UNSAFE.putObject(this, itemOffset, item);
        }
		// 修改节点的item
        boolean casItem(E cmp, E val) {
            return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
        }
		// 懒修改节点的next
        void lazySetNext(Node<E> val) {
            UNSAFE.putOrderedObject(this, nextOffset, val);
        }
		// CAS修改节点的next节点
        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);
            }
        }
    }

因为每个节点会将元素封装成一个Node,将元素保存在item中,将下一个元素的引用保存在next中,从而实现链表结构,元素按照 FIFO 的形式来访问,队列头部为待的时间最久的元素,尾部则是最少,新元素插在尾部。

不同于LinkedBlockingQueue中对Node的操作,如果需要对Node进行操作,需要通过CAS方式进行修改,不能直接进行修改,否则不能保证线程安全性

ConcurrentLinkedQueue源码分析

ConcurrentLinkedQueue主要成员变量

	// 头节点
    private transient volatile Node<E> head;
	// 尾节点
    private transient volatile Node<E> tail;

这里先简单的说一下head指向的不一定是最前面的节点,tail也不一定是最末尾的节点

ConcurrentLinkedQueue构造方法

    public ConcurrentLinkedQueue() {
        head = tail = new Node<E>(null);
    }

初始化的时候会首先创建一个空节点,从这个空节点开始进行入队和出队

ConcurrentLinkedQueue添加元素

ConcurrentLinkedQueue–offer

add实际上调用子类的offer方法添加数据,因为继承了AbstractQueue抽象类

    public boolean offer(E e) {
    	// 判断是否为空元素
        checkNotNull(e);
        // 将添加元素封装成Node节点
        final Node<E> newNode = new Node<E>(e);

		// while循环,获取tail并赋给局部变量t,同时将t赋给p
        for (Node<E> t = tail, p = t;;) {
        	// 获取p的next节点
            Node<E> q = p.next;
            // 判断下一个节点是否为空
            if (q == null) {
                // p is last node
                // 为空,说明p后面没有元素了,说明p是最后一个节点
                // 通过cas方式设置p的next为新添加的元素
                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".
                    // 判断p是否和t相等
                    if (p != t) // hop two nodes at a time
                    	// 如果不相等,重新将tail设置成最新的节点
                        casTail(t, newNode);  // Failure is OK.
                    // 返回
                    return true;
                }
                // Lost CAS race to another thread; re-read 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是否和刚添加元素的时候的t相等,如果不相等,将t赋给p,如果相等将head赋给p
                p = (t != (t = tail)) ? t : head;
            else
                // Check for tail updates after two hops.
                // 判断t和p是否相等,如果不相等,说明p移动了,如果相等说明t还能用
                // 判断tail和添加元素时候的t是否相等,如果不相等,说明tail让其他线程改变了,重新定位p的位置为q
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

其实干说起来上面的代码还是比较蒙的,接下来,通过图片一一讲解一下

首先初始化的时候
在这里插入图片描述
当第一个线程添加元素的时候,while循环开始初始化
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
第一添加完元素的状态,当再次添加元素的时候,tail的位置没有改变,所以此时获取的位置还是初始的位置,此时在获得p的next的时候就不为空,next为刚才新添加的节点

所以进入最后一个else,也就是p = (p != t && t != (t = tail)) ? t : q;

首先判断p是否等于t,由于此时只有一个线程,所以p和t是相等的,p没有移动,所以此时会返回false,会将q赋给p
在这里插入图片描述
此时进入第二遍循环的时候,p指向刚才添加的节点,q指向null,此时进入第一if判断,通过cas设置p的next指向新添加的节点
在这里插入图片描述
此时的链表结构如上,通过cas方式设置了p的后继节点,然后判断此时的p和t是否相等,发现现在p和t是不相等的,所以需要更新tail为最新的节点,通过cas方式设置newNode1为tail节点,返回true
在这里插入图片描述
以上方法的分析只是线程顺序执行的效果,在并发情况下,再次分析一下

假如线程1和线程2同时获取链表执行初始状态,线程1向链表中添加了两个元素之后,线程2才开始添加元素

在这里插入图片描述
线程2在判断链表的时候发现线程1已经修改链表了

在这里插入图片描述
线程2判断的时候,发现q不为null,进入else,判断p和t是否相等,如果相等,则移动p的位置
在这里插入图片描述
再次循环,q依然不为空,进入else,判断p和t是否相等,如果不相等,进行t != (t = tail)的判断,其实这句话也好理解,就是获取当前最新的tail节点的位置,赋值给t,判断tail现在的位置是否和线程当初进行while循环时是否相等,如果不相等,说明tail的位置移动了,所以需要重新定位tail的位置为最新的位置,并赋值给t,同时重新定位p的位置为最新tail的位置
在这里插入图片描述
所以通过上面的分析,我们发现通过tail引用定位了线程2应该添加元素的位置

以上就是对于多线程下如何进行元素添加的分析,其实这里还有一个问题就是p==q,这种情况,想要知道这种情况,就需要知道出队的时候进行了什么操作,才能理解

ConcurrentLinkedQueue–poll

接下来我们看一下出队的方法poll

    public E poll() {
        restartFromHead:
        // 外层while循环
        for (;;) {
        	// 内层while循环,首先获取head节点,赋给h,同时将h赋给p,初始化q
            for (Node<E> h = head, p = h, q;;) {
            	// 获取p保存的元素item
                E item = p.item;

				// 判断item是否为空,如果不为空的同时,设置p保存的item为null,如果都成功,说明已经成功清除了p上面保存的数据
                if (item != null && p.casItem(item, null)) {
                    // Successful CAS is the linearization point
                    // for item to be removed from this queue.
                    // 判断p和h是否相等
                    if (p != h) // hop two nodes at a time
                    	// 如果不相等,更新head,将p的后继节点赋给q,同时判断后继节点是否为空
                    	// 如果不为空,说明p节点的后继节点可能是head节点
                    	// 如果为空,说明p节点后面没有节点了,那么说明p就是head节点
                        updateHead(h, ((q = p.next) != null) ? q : p);
                    return item;
                }
                // 如果发现p的后继节点为空,说明p是新的head
                else if ((q = p.next) == null) {
                    updateHead(h, p);
                    return null;
                }
                // 如果发现p的后继节点和p相等
                else if (p == q)
                	// 重新进行while循环,获取最新的head后再取出数据
                    continue restartFromHead;
                else
                	// 移动p,为后继节点
                    p = q;
            }
        }
    }

再看一下updateHead都做了什么,通过方法名,知道是更新head节点

    final void updateHead(Node<E> h, Node<E> p) {
    	// 判断h和p是否相等,不相等才有替换的必要
    	// 通过cas设置p
        if (h != p && casHead(h, p))
        	// 如果成功,通过cas方式将h的next设置为自己
            h.lazySetNext(h);
    }

通过观察上面方法,我们知道了为什么有的时候需要判断p == q,因为在调用updateHead的时候会调用h.lazySetNext(h)方法,next引用自己,目的是将这个节点脱离链表,没有引用指向这个节点后,通过垃圾回收机制将这个对象清除掉,所以在发生p == q的时候需要重新定位tail和head的位置,保证获取到最新的节点信息

接下来看一下在单线程条件下,元素是如何出队的

进行里层while循环的时候状态如图所示,此时队列中没有数据,进入第二个if判断,p的后继节点是否为空,如果为空设置head为p,返回null后直接跳出循环
在这里插入图片描述
当队列中含有数据的时候,再次执行poll的时候,判断p中存储的元素是否为null,如果为空,进入第二个if判断,首先获取p的后继节点q,判断q是否为空,q不为空,所以再次进行第三个if判断,p和q是否相等,显然是不相等的,所以进入最后一个else,将q赋值给p

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
再次进入while循环的时候,此时获取p的元素,不为空,通过cas方式设置p保存的item为空,如果成功说明这个元素可以返回

再判断p和h是否相等,此时如果不相等,说明p的位置移动了,需要更新head节点的位置,此时判断p的后继节点是否为空,如果不为空,说明此时的节点应该脱离队列,p后面的后继节点应该为head节点,如果为空,说明p后面已经没有数据了,那么此时p就应该是head节点,将head设置为p,后返回item
在这里插入图片描述
当再次获取元素的时候

在这里插入图片描述
这是在单线程的条件下执行,我们理解了,当在多线程的情况下是如何运行的呢?

线程1和线程2同时获取数据,线程1获取完元素之后,更新head节点的位置,此时的线程2判断,p存储的元素是否为空,如果为空,则设置q等于p进行下一次循环
在这里插入图片描述
此时的线程2进行第二次循环的时候,获取的是线程1操作完的状态,线程2在进行第二次循环的时候,判断p保存的元素是否为空,此时判断元素为空,因为线程1在获取完元素会通过CAS把item设置为空,之后线程2再进行判断的时候判断为空

在这里插入图片描述
进入第二个if判断,将p的后置节点赋给q,判断q是否为空,因为p的后置节点指向自己,所以q不为空,这时在进入第三个if判断,此时判断p和q是否相等,因为q指向自己,所以此时p和q是相等的,跳出里层while循环,重新进行外层while循环,也就是重新获取head节点的位置,重新从最新的head节点开始进行获取元素

在这里插入图片描述
通过上面的分析,我们知道了,为什么会有p等于q的情况,实际上就是链表在丢弃数据的时候,为了将丢弃的数据进行垃圾回收,导致后置节点指向自己的情况发生,从而需要重新获取此时的head节点的最新位置,进行数据取出操作

我们现在再回到插入数据的时候,在插入数据的时候,从tail开始插入的时候,刚好判断p的后继节点q等于自己,说明此时的节点已经脱离链表了,需要重新定位此时p的位置

首先判断tail的位置是否和刚刚进入到while循环的时候的位置相等,如果相等,则定位p的位置为head的位置,如果不相等,则定位p的位置为tail最新的位置

在这里插入图片描述
此时判断p与q是相等的,并且tail的位置没有移动,说明整个这个节点都是不可用的,整个节点都从链表中脱离了,所以需要重新定位tail的位置,所以需要从head的位置重新定位p和tail的位置

在这里插入图片描述
同样的,此时p与q是相等的,并且tail的位置已经通过别的线程移动了,同样的此时这个节点也是不可用的,整个节点同样的需要从链表中脱离,但是发现此时的tail节点的位置改变了,所以应该使用最新的tail节点进行增加操作,所以从最新的tail开始,进行后续节点的更新

通过上面的分析,我们同时还可以得出tail有时候会滞后于head的结论,因为此时的tail节点已经脱离的队列,所以需要重新从head开始,重新进行节点位置的定位

ConcurrentLinkedQueue–remove

接下来再简单的看一下remove方法

    public boolean remove(Object o) {
    	// 判断是否为空
        if (o != null) {
        	// 初始化指针
            Node<E> next, pred = null;
            // 获取链表第一个节点,一直遍历到链表尾部,p为空为止
            // 赋值临时变量p给pred,同时将next赋给p
            for (Node<E> p = first(); p != null; pred = p, p = next) {
                boolean removed = false;
                // 获取p保存的元素
                E item = p.item;
                // 如果不为空
                if (item != null) {
                	// 判断item与o是否相等
                	// 如果不相等
                    if (!o.equals(item)) {
                    	// 获取p的下一个元素
                        next = succ(p);
                        // 继续下一次循环
                        continue;
                    }
                    // 如果相等,通过cas将p的item设置为null
                    removed = p.casItem(item, null);
                }
				
				// 同时获取p的下一个元素
                next = succ(p);
                // 如果p刚才保存的元素和next保存的元素都不为空
                if (pred != null && next != null) // unlink
                	// 通过cas方式将pred的next设置为最新的next
                    pred.casNext(p, next);
                // 如果cas成功,则返回移除成功
                if (removed)
                    return true;
            }
        }
        return false;
    }

首先简单的看一下succ方法

    final Node<E> succ(Node<E> p) {
        Node<E> next = p.next;
        return (p == next) ? head : next;
    }

其实在了解元素出队列的特殊情况,再来看这段代码就非常简单了,首先获取p的后继节点,如果p的后继节点和p相等,说明这个元素已经脱离队列了,所以返回head节点,如果不相等,说明这个元素还在链表中,返回p的后继节点

之后再看一下first方法

    Node<E> first() {
        restartFromHead:
        // 外层while循环
        for (;;) {
        	// 获取head节点,初始化临时变量h,p,q
            for (Node<E> h = head, p = h, q;;) {
            	// 判断p保存的元素是否为空,如果不为空
            	// 判断p的后继节点是否为空
            	// 如果上面两个条件有一个成立,则更新head为p节点
                boolean hasItem = (p.item != null);
                if (hasItem || (q = p.next) == null) {
                    updateHead(h, p);
                    // 如果有元素则返回p
                    // 如果没有元素,则返回null
                    return hasItem ? p : null;
                }
                // 如果判断p等于q,说明脱离链表了,重新定位元素位置
                else if (p == q)
                    continue restartFromHead;
                else
                	// 其他情况定位p为下一个节点,进行下一次循环
                    p = q;
            }
        }
    }

通过上面出队代码的分析,我们知道队列可能会有两种情况
在这里插入图片描述
head有可能是哨兵节点,也有可能是保存元素的节点

ConcurrentLinkedQueue–size

    public int size() {
        int count = 0;
        // 从head开始遍历
        for (Node<E> p = first(); p != null; p = succ(p))
        	// 判断p保存的元素是否为空
            if (p.item != null)
                // Collection.size() spec says to max out
                // 如果不为空,将count数量加一
                // 如果等于Integer.MAX_VALUE则跳出循环,返回count
                if (++count == Integer.MAX_VALUE)
                    break;
        return count;
    }

size没有加锁,就是遍历了整个队列,但是遍历的同时可能在发生poll或者offer,所以size不是特别的精确,用的时候要注意

ConcurrentLinkedQueue–总结

  1. ConcurrentLinkedQueue通过CAS保证链表的线程的安全
  2. ConcurrentLinkedQueue具有可变性和不可变性
  3. 不变形:head和tail都可以通过succ方法访问到;head != null;tail != null
  4. 可变性:head.item可能是null,也可能不是null;tail.item可能是null,也可能不是null;允许tail滞后于head;tail.next有可能指向tail
  5. ConcurrentLinkedQueue允许队列处于不一致状态,分离了poll和offer两个操作
  6. ConcurrentLinkedQueue与LinkedBlockingQueue对比,LinkedBlockingQueue能通过count来获取队列中元素准确的数量,因为添加和取出的时候都会对count进行修改,而且整个操作是原子性的,所以可以通过count返回队列中元素的个数,但是ConcurrentLinkedQueue对队列进行修改的时候不能保证原子性,所以导致不能对count进行修改,只能通过一种全部遍历的方式获取不准确的数值
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值