ConcurrentLinkedDeque源码解析

时间:2022-07-23
本文章向大家介绍ConcurrentLinkedDeque源码解析,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

Java的并发包JUC大概包括五大部分,分别是原子操作类、锁机制、并发集合、并发框架和并发集合。在之前大概的阅读了前两者的实现过程。接下来准备读一读并发集合相关的实现。之前我们研究原子类的时候基本的实现是借助于valative和CAS来实现的,那么对于集合又如何让其在高并发的时候能够高效率的存储和获取,为什么这么说,难道JUC做的那么多并发集合就是为了解决这个问题?因为之前我们我们在锁的那块说过,如果加锁之后副本代码只能择一进行运行,所以显然并发集合肯定不是咋理解的那样,准确的说,并发集合是用来在并发量高的是。最基本是当一个线程修改或者删除了集合中的数据。其他线程可能获取的值变为脏数据。所以这块的并发集合就是要解决高并发情况下的存储和获取,以至于在高并发情况下能够尽量提升系统性能。

ConcurrentLinkedDeque是一个高并发链表,从上边的截图中可以看到。ConcurrentLinkedDeque的实现是采用Unsafe来实现的,其中包括了head和tail,而head和tail是长整形的类型。那么我们大概可以猜测出该类对链表的节点的操作是采用的Unsafe来操作的,而操作的参数就是head和tail的偏移量。如果采用Unsafe操作,那么我们需要看一下这个链表的节点定义,按照之前的学习,那么node应该是线程可见的。

在Node节点中定义中,我们看到链表的一般结构。但是我们发现在链表中我们依然看到了偏移量的定义,按照我们之前的想法,我觉得用一个偏移量就可以了,就是记录整个链表的的首和尾.

但是仔细想确实有问题,如果我们要操作中间一个节点的后置或者前置,那要去通过首和尾的偏移量去计算吗,如何中间某个节点足够大,那么根本没办法知道节点的开始和结束的位置,而且我们操作是基于Unsafe的,所以地址偏移量很重要。需要在每个节点记录一次。除此之外,node的每个节点提供对节点的CAS操作的方法。其实现的过程和我们在原子类中的CAS分析一样。这里不再赘述。

在剩余的内部类中,我们看到了迭代器。这里的迭代器就相当于ConcurrentLinkedDeque的专用迭代器。这里我们先不看迭代器,我们先看对链表操作的一般步骤就是增删改查的操作。

    private void linkFirst(E e) {
        checkNotNull(e);
        final Node<E> newNode = new Node<E>(e);


        restartFromHead:
        for (;;)
            for (Node<E> h = head, p = h, q;;) {
                if ((q = p.prev) != null &&
                    (q = (p = q).prev) != null)
                    // Check for head updates every other hop.
                    // If p == q, we are sure to follow head instead.
                    p = (h != (h = head)) ? h : q;
                else if (p.next == p) // PREV_TERMINATOR
                    continue restartFromHead;
                else {
                    // p is first node
                    newNode.lazySetNext(p); // CAS piggyback
                    if (p.casPrev(null, newNode)) {
                        // Successful CAS is the linearization point
                        // for e to become an element of this deque,
                        // and for newNode to become "live".
                        if (p != h) // hop two nodes at a time
                            casHead(h, newNode);  // Failure is OK.
                        return;
                    }
                    // Lost CAS race to another thread; re-read prev
                }
            }
    }

在将元素添加到头部节点的时候,我们一般的操作是将头部的指针指向该节点,然后让该节点的next指向原来的头节点的next节点。大概的过程是这样的。但是上边的操作的都是基于Unsafe的,上边的代码是先找到head,而多线程情况下可能head一直在变的缘故吧,这里拿到head之后好像是继续往前走,直到找到真的头节点,然后才进行casPrev操作,成功之后才进行头节点的设置。如果设置失败,也就是当前线程找到的头已经被其他线程修改了,那么就自旋,继续向前找。直到设置成功。通过linkFirst的阅读,那么liskLast的操作应该也差不多

       public E pollFirst() {
        for (Node<E> p = first(); p != null; p = succ(p)) {
            E item = p.item;
            if (item != null && p.casItem(item, null)) {
                unlink(p);
                return item;
            }
        }
        return null;
    }

在pollFirst中,首先拿到头接点,然后去判断是否是头节点,如果是的话就将头节点释放,然后将头简单返回。那么pollLast方法也差不多。

在push、pop提供了栈的用法。toArrayList也没有什么要写的了,都是常规操作。看了看,好像没啥了。好了今天的分析就到这里了。晚安!