Java多线程(二十五)---ConcurrentLinkedQ

2018-10-08  本文已影响26人  凯玲之恋

移步java多线程系列文章

让我们一起来研究一下Doug Lea是如何使用非阻塞的方式来实现线程安全队列ConcurrentLinkedQueue的

ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部;当我们获取一个元素时,它会返回队列头部的元素。它采用了“wait-free”算法(即CAS算法)来实现,该算法在Michael&Scott算法上进行了一些修改。

1 ConcurrentLinkedQueue的结构

ConcurrentLinkedQueue的类图来分析一下它的结构


ConcurrentLinkedQueue的类图.jpg

ConcurrentLinkedQueue由head节点和tail节点组成,每个节点(Node)由节点元素(item)和指向下一个节点(next)的引用组成,节点与节点之间就是通过这个next关联起来,从而组成一张链表结构的队列。默认情况下head节点存储的元素为空,tail节点等于head节点。

private transient volatile Node<E> tail = head;

2 入队列

2.1 入队列的过程

qq_pic_merged_1538983662965.jpg

发现入队主要做两件事情

通过对上面的分析,我们从单线程入队的角度理解了入队过程,但是多个线程同时进行入队的情况就变得更加复杂了,因为可能会出现其他线程插队的情况。

如果有一个线程正在入队,那么它必须先获取尾节点,然后设置尾节点的下一个节点为入队节点,但这时可能有另外一个线程插队了,那么队列的尾节点就会发生变化,这时当前线程要暂停入队操作,然后重新获取尾节点

让我们再通过源码来详细分析一下它是如何使用CAS算法来入队的。

public boolean offer(E e) {
        if (e == null) throw new NullPointerException();
        // 入队前,创建一个入队节点
        Node<E> n = new Node<E>(e);
        retry:
        // 死循环,入队不成功反复入队。
        for (;;) {
            // 创建一个指向tail节点的引用
            Node<E> t = tail;
            // p用来表示队列的尾节点,默认情况下等于tail节点。
            Node<E> p = t;
            for (int hops = 0; ; hops++) {
            // 获得p节点的下一个节点。
                Node<E> next = succ(p);
    // next节点不为空,说明p不是尾节点,需要更新p后在将它指向next节点
                if (next != null) {
                    // 循环了两次及其以上,并且当前节点还是不等于尾节点
                    if (hops > HOPS && t != tail)
                        continue retry; 
                    p = next;
                }
                // 如果p是尾节点,则设置p节点的next节点为入队节点。
                else if (p.casNext(null, n)) {
                    /*如果tail节点有大于等于1个next节点,则将入队节点设置成tail节点,
                      更新失败了也没关系,因为失败了表示有其他线程成功更新了tail节点*/
if (hops >= HOPS)
                        casTail(t, n); // 更新tail节点,允许失败
                    return true;
                }
                // p有next节点,表示p的next节点是尾节点,则重新设置p节点
                else {
                    p = succ(p);
                }
            }
        }
    }

从源代码角度来看,整个入队过程主要做两件事情:

2.2 定位尾节点

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

2.3 设置入队节点为尾节点

p.casNext(null,n)方法用于将入队节点设置为当前队列尾节点的next节点,如果p是null,表示p是当前队列的尾节点,如果不为null,表示有其他线程更新了尾节点,则需要重新获取当前队列的尾节点。

2.4 HOPS的设计意图

上面分析过对于先进先出的队列入队所要做的事情是将入队节点设置成尾节点,doug lea写的代码和逻辑还是稍微有点复杂。那么,我用以下方式来实现是否可行?

public boolean offer(E e) {
        if (e == null)
                        throw new NullPointerException();
                Node<E> n = new Node<E>(e);
                for (;;) {
                        Node<E> t = tail;
                        if (t.casNext(null, n) && casTail(t, n)) {
                                return true;
                        }
                }
    }

private static final int HOPS = 1;

注意 入队方法永远返回true,所以不要通过返回值判断入队是否成功。

3 出队列

参考

《java并发编程的艺术》

上一篇 下一篇

猜你喜欢

热点阅读