(七)如何理解线程安全的ConcurrentLinkedQueue队列的底层源码实现?

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_36520235/article/details/83243641

(一)先了解一下ConcurrentLinkedQueue是怎么来实现的线程安全的队列

从以下几个方面去讨论:
(1)ConcurrentLinkedQueue的简单介绍和用途:
(2)ConcurrentLinkedQueue是如何实现线程安全的入队列
(3)ConcurrentLinkedQueue是如何实现线程安全的出队列
(4)对ConcurrentLinkedQueue的总结和思考

(1)ConcurrentLinkedQueue的简单介绍和用途

在单线程编程中我们会经常用到一些集合类,比如ArrayList,HashMap等,但是这些类都不是线程安全的类。在面试中也经常会有一些考点,比如ArrayList不是线程安全的,Vector是线程安全。而保障Vector线程安全的方式,是非常粗暴的在方法上用synchronized独占锁,将多线程执行变成串行化。要想将ArrayList变成线程安全的也可以使用Collections.synchronizedList(List list)方法ArrayList转换成线程安全的,但这种转换方式依然是通过synchronized修饰方法实现的,很显然这不是一种高效的方式,同时,队列也是我们常用的一种数据结构,为了解决线程安全的问题,Doug Lea大师为我们准备了ConcurrentLinkedQueue这个线程安全的队列。

可以先看一下他的继承关系,继承的是AbstractQueue,实现的是Queue的接口
在这里插入图片描述

在这里插入图片描述

(2)ConcurrentLinkedQueue是如何实现线程安全的入队列

首先先对照着入队列的源码来看一下实现的思路:

  1. offer这个方法是用来入队列的
 public boolean offer(E e) {
   // 0.这里是检查入队列前要插入的节点是不是为空
        checkNotNull(e);
       // 1. 这是入队前,先创建一个入队节点
        final Node<E> newNode = new Node<E>(e);
		//2. 这里是一个死循环,如果入队不成功继续尝试入队
        for (Node<E> t = tail, p = t;;) {
        //3.这里是判断尾节点给q,然后进行判空
            Node<E> q = p.next;
            if (q == null) {
                // 4. p表示尾节点,下面这句表示看尾节点的下一个节点是不是为空,
//casNext就表示P的下一个节点如果为空,就新建一个节点给尾节点的下一个节点(这个节点就是要入队的新节点),如果不为空,就不用管(说明已经有节点了)
                if (p.casNext(null, newNode)) {
//5.这个是判断尾节点和tail节点是不是指向的是同一个节点,这个是更新tail节点
                    if (p != t) // hop two nodes at a time
                        casTail(t, newNode);  // Failure is OK.
                    return true;
                }
            }
            //(这里其实又是另外一种场景)说明p指向的节点的next也指向它自己,这种节点称之为哨兵节点,这种节点在队列中存在的价值不大,一般表示为要删除的节点或者是空节点
            else if (p == q)
            
                p = (t != (t = tail)) ? t : head;
            else
                // Check for tail updates after two hops.
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

入队列主要围绕着两步:

1. 把入队节点当成当前队列尾节点的下一个节点
2. 更新tail节点:(1)如果tail节点的下一个节点不为空,说明已经有了一个要入队的节点了,此时则将此时的入队节点设置为tail节点 ,例如下图中的添加元素1的图 (2)如果tail节点的下一个节点为空,例如添加元素2的图中,此时则将要入队列的节点设置为tail的下一个节点
3. 补充:因为如果在多线程的情况下,可能A线程刚完成第一步的操作,此时B线程过来了,看到tail节点已经发生了变化,这是A线程要准备操作第二更新tail节点的时候,此时A线程就会暂停入队操作,然后需要通过CAS重新获取尾节点

在这里插入图片描述

(3)ConcurrentLinkedQueue是如何实现线程安全的出队列

出队列的几个步骤:

1. 如果当前head,h和p指向的节点的Item不为null的话,说明该节点即为真正的队头节点(待删除节点),只需要通过casItem方法将item域设置为null,然后将原来的item直接返回即可。

2. 如果当前head,h和p指向的节点的item为null的话,则说明该节点不是真正的待删除节点,那么应该做的就是寻找item不为null的节点。通过让q指向p的下一个节点(q = p.next)进行试探,若找到则通过updateHead方法更新head指向的节点以及构造哨兵节点(通过updateHead方法的h.lazySetNext(h))

public E poll() {
    // 如果出现p被删除的情况需要从head重新开始
    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)
                // 当一个线程在poll的时候,另一个线程已经把当前的p从队列中删除——将p.next = p,p已经被移除不能继续,需要重新开始
                continue restartFromHead;
            else
                p = q;
        }
    }
}

final void updateHead(Node<E> h, Node<E> p) {
    if (h != p && casHead(h, p))
        h.lazySetNext(h);
}

出队列详细图:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_36520235/article/details/83243641