ConcurrentLinkedQueue

作用

支持并发操作的线程安全队列,正如其名linKedQueue, 说明这个队列是用链表实现的队列, 对于队列我们直到主要的操作就是入队列和出队列,入队和出队操作同一个链表必要会有线程安全问题,如何避免:入队和出队方法直接加synchronized锁, 可以, 但是效率太低了, 入队和出队 在链表头尾,直接有头尾指针指向,这也说明入对出队时间是极短,并不需要进行耗时业务操作,如果直接加synchronized 发生竞争就会变成重量级锁,说不定入队耗时,还没有加锁释放锁进行内核态切换耗时多,所以面对这种需要线程安全,但是耗时又极短,最好的方法就是使用cas和循环补偿。 所以ConcurrentLinkedQueue队列的所有线程安全为了避免锁带来的消耗,全是使用的cas和自旋。

呃, 虽然cas和自旋 在这种情况下效率高于synchronized,但是问题就是编程很复杂。。。。。

思考优化点

呃, 既然是链表,为了入队和出队去循环,所以ConcurrentLinkedQueue 里面有头尾指针,这也就能直接得到头和尾, 入队就是把当前节点链接到尾节点,出队就是把当前节点链表到后继。那么思考一个问题, head 和 tail 指针 需要每次入队出队都要移动吗, 也就是 说实现方式要用 cas 抢夺 tail 然后 pre.next = tail。
作为一个并发链表队列, 如果1000个线程都进行入队操作,那么tail就要进行1000次有移动, 所以效率上有一些影响,同意的出队也有同样的问题。
需要有一个哨兵节点, 这也就不需要考虑head和tail之间在队列中有没有节点插入和删除的问题。各自做各自的。

数据结构

链表

private static class Node<E> {
        volatile E item; //值 
        volatile Node<E> next; //指向下一个节点
  }

Node中的个个方法

呃 我们直到 node就是链表中的元素, 所以入队和出队等操作,处理要处理(看情况 即有没有达到处理阈值)head和tail 之外,还需要处理node的next指向, 包括 入队的时候前一个节点的next指向入队节点, 以及入队节点item的赋值。 以及出队item的赋值为null, next指向自己(从链表中断开)。

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); //普通变量的写,节点初始化,没有入队,不会共享,所以不需要volatile的写保障可见。普通变量的写效率高于volatile的写
        }

        boolean casItem(E cmp, E val) {
            return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);//cas设置item的值 比如:出队的时候, 多个线程对同一个节点进行出队,都需要给该节点的item设为null, 只能有一个成功。
        }

        void lazySetNext(Node<E> val) { //出队的时候,需要把next指向自己, 但是由于头指向以及指向出队节点的下一个节点,所以当前节点的next的不需要很强的可见性
            UNSAFE.putOrderedObject(this, nextOffset, val);//putOrderedObject 取决于编译器要不要优化给可见性。
        }

        boolean casNext(Node<E> cmp, Node<E> val) { //修改next
            return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
        }

        // Unsafe mechanics

        private static final sun.misc.Unsafe UNSAFE; //魔法类
        private static final long itemOffset; //item 在Node中的地址偏移量
        private static final long nextOffset; //next在Node中的地址偏移量

        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);
            }
        }
    }

构造函数

public ConcurrentLinkedQueue() {
        head = tail = new Node<E>(null); //节点的初始化
    }

队列中, 存在一个哨兵节点

offer 入队 (链表是无界,所以次方法永远不会返回false)

public boolean offer(E e) { //队列 入队
        checkNotNull(e); //入队的值 不能为null
        final Node<E> newNode = new Node<E>(e); //创建节点 这里的final保证了 newNode拿到的时完整的对象, 即final变量 禁止编译重排的影响

        for (Node<E> t = tail, p = t;;) { //无锁 入队,cas+循环补偿 思考:这里入队为啥不是cas tail指向newNode, 成功之后在把之前的tail.next = newNode
            Node<E> q = p.next;
            if (q == null) { //p是队列末尾
                // p is last node
                if (p.casNext(null, newNode)) { //cas入队, 失败的需要继续循环
                    // 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 t 和 p 之间需要至少2个节点的阈值
                        casTail(t, newNode);  // Failure is OK. cas 设置尾节点
                    return true;
                }
                // Lost CAS race to another thread; re-read next  cas失败,那么说明当前节点的next指针已经指向别的node了, 当前线程重新循环获取最新的next
            } else if (p == q) //这种情况比较特殊,要和入队进行比较 能出现这种情况说明 p节点是以被别的线程出队了
                // 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 //p 不为null, 说明别的线程以及offer成功了
                // Check for tail updates after two hops. 得p 在第二个节点才会去判断头又没有改变
                p = (p != t && t != (t = tail)) ? t : q; //如果别的线程offer成功了,并且修改了tail, p指向最新的tail 这样就可以快速到达尾巴节点提升效率, 否则 p 指向自己的后继q
        }
    }

因为是无锁队列,所以入队和出队, 入队和入队, 出队和出队都是同时进行的, 这里为啥会出翔 p==q 等 继续看poll()方法,因为和出队有关系

poll

/*出队从对头*/
    public E poll() {
        restartFromHead:  //标签
        for (;;) {           //cas 补偿机制
            for (Node<E> h = head, p = h, q;;) {
                E item = p.item; //记录原先值

                if (item != null && p.casItem(item, null)) { //如果item不为空并且cas修改成功
                    // Successful CAS is the linearization point
                    // for item to be removed from this queue.
                    if (p != h) // hop two nodes at a time 同样和入队方法一样, 移动头指针存在至少2个节点的阈值
                        updateHead(h, ((q = p.next) != null) ? q : p);//如果后继不为null那么head执行后继,否者head指向当前节点
                    return item;
                }
                else if ((q = p.next) == null) {  //说明到了尾节点, p的所有前驱节点item都被别的线程设置为了null
                    updateHead(h, p); //帮助别的线程移动头指针
                    return null;
                }
                else if (p == q)//说明p节点已经被别的线程poll了
                    continue restartFromHead; //重新从head开始遍历
                else
                    p = q;//指向自己的后继
            }
        }
    }

updateHead

final void updateHead(Node<E> h, Node<E> p) {
        if (h != p && casHead(h, p)) //cas需改head指向
            h.lazySetNext(h); //next指向自己,断开, 因为head已经指向自己的后继,所以如果有后面来的线程,他们读head,所以next不需要强可见性。
    }

解释 为啥入队出队 p == q

updateHead的时候会把原先节点的next指向自己, 如果这个lazySetNext不是立即刷内存,但是如果cpu自己刷了,那别的线程就可以看到。
在这里插入图片描述

peek 查看对头item

//查看对头节点的值
    public E peek() {
        restartFromHead: //标签
        for (;;) {
            for (Node<E> h = head, p = h, q;;) { //指向head 
                E item = p.item; //volatile的读
                if (item != null || (q = p.next) == null) {//item不为空 或则和 p的后继为null
                    updateHead(h, p); //更新head 
                    return item;
                }
                else if (p == q) //p节点已经出队
                    continue restartFromHead; //重新从head 找对头
                else //继续遍历后继
                    p = q;
            }
        }
    }

first 获取对头node

Node<E> first() {
        restartFromHead:
        for (;;) {
            for (Node<E> h = head, p = h, q;;) {
                boolean hasItem = (p.item != null);
                if (hasItem || (q = p.next) == null) {
                    updateHead(h, p);
                    return hasItem ? p : null;
                }
                else if (p == q)
                    continue restartFromHead;
                else
                    p = q;
            }
        }
    }

peek和first 一个是返对头item , 一个是返回对头Node, 为啥要写两个方法,peek为啥不写 first().item??, peek和first两个方法在和poll方法都是同时可以进行的,因为是无锁, 这样就会导致 first的出来的Node节点会存在刚刚好被poll的, 所以item为null, 而peek方法返回null,除非是队列到了队列末尾整所有节点都是item为null, 所以peek 不能直接复用 first.item , 如果非要使用还需要判断first.item如果为null, 继续进行for循环

remove 从队列中删除一个元素

public boolean remove(Object o) {
        if (o != null) {
            Node<E> next, pred = null;
            for (Node<E> p = first(); p != null; pred = p, p = next) {//p指向对头节点 pred为p的前驱
                boolean removed = false; //是否移除标志
                E item = p.item;
                if (item != null) { //没有被被别的线程poll或者remove
                    if (!o.equals(item)) { //是否和目标移除对象相等
                        next = succ(p); //获取后继  注意:如果p被poll了他的后继就是自己, 所以这种情况下p的后继为head
                        continue;
                    }
                    removed = p.casItem(item, null); //进行cas移除 是否能够竞争过别的线程
                }

                next = succ(p); //获取后继
                if (pred != null && next != null) // unlink 断开连接
                    pred.casNext(p, next); 
                if (removed) //移除了
                    return true;
            }
        }
        return false;
    }

size 获取队列元素个数

public int size() {
        int count = 0;
        for (Node<E> p = first(); p != null; p = succ(p)) //从栈顶进行遍历,取后继
            if (p.item != null) //item不为空
                // Collection.size() spec says to max out
                if (++count == Integer.MAX_VALUE) //没有找过最大值
                    break;
        return count;
    }

此方法直接遍历判断 item不为null, 直接数量+1, 如果遍历到后面前面的节点被别的线程poll了, 所以数量是不准确的, 所以在这种无锁并发队列中 获取队列元素个数的方法不是准确的,所以用处不怎么大,这个size方法, 而且size遍历整个队列,效率也不高

isEmpty判断队列是否包含元素

前面提到的size方法如果用来判断队列是否包含元素是不妥的,因为size方法计数过程中是不准确的。

public boolean isEmpty() {
        return first() == null; //获取栈顶元素
    }

迭代器

呃,迭代器的实现不不是强一致性,因为整个队列是并发无锁的,所以在迭代的时候别的线程进行poll remove offer等操作, 所以迭代器的实现就是每次hashNext的时候都是取获取最新的队列头节点。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值