源码解读之(八)ConcurrentLinkedQueue

源码解读之(八)ConcurrentLinkedQueue

    • 一、前言
    • 二、ConcurrentLinkedQueue介绍与结构
    • 三、offer操作
    • 四、poll操作
    • 五、peek操作
    • 六、size操作
    • 七、remove操作
    • 八、ConcurrentLinkedQueue遇到的问题
    • 九、总结

一、前言

在并发编程中我们有时候需要使用线程安全的队列。如果我们要实现一个线程安全的队列有两种实现方式:一种是使用阻塞算法,另一种是使用非阻塞算法。使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现,而非阻塞的实现方式则可以使用循环CAS的方式来实现,本文让我们一起来研究下Doug Lea是如何使用非阻塞的方式来实现线程安全队列ConcurrentLinkedQueue的,相信从大师身上我们能学到不少并发编程的技巧。

二、ConcurrentLinkedQueue介绍与结构

1、ConcurrentLinkedQueue整体属性与内部类
源码解读之(八)ConcurrentLinkedQueue_第1张图片
其中有一个很重要的内部类Node

源码解读之(八)ConcurrentLinkedQueue_第2张图片

// 用于存放节点的值
volatile E item;
// 指向下一个节点
volatile Node<E> next;
// 这里也是用的是UNSAFE类,这个类直接提供CAS操作
private static final sun.misc.Unsafe UNSAFE;
// item字段的偏移量
private static final long itemOffset;
// next的偏移量
private static final long nextOffset;

2、ConcurrentLinkedQueue类图结构

源码解读之(八)ConcurrentLinkedQueue_第3张图片

ConcurrentLinkedQueue中有两个volatile类型的Node节点分别用来存放列表的首尾节点,其中head节点存放链表第一个itemnull的节点,tail则并不是总指向最后一个节点。Node节点内部则维护一个变量item用来存放节点的值,next用来存放下一个节点,从而链接为一个单向无界列表

注意,默认情况下头节点尾节点都是哨兵节点,也就是一个存nullNode节点。

// 存放链表的头节点
private transient volatile Node<E> head;
// 存放链表的尾节点
private transient volatile Node<E> tail;
// UNSAFE对象
private static final sun.misc.Unsafe UNSAFE;
// head字段的偏移量
private static final long headOffset;
// tail字段偏移量
private static final long tailOffset;
public ConcurrentLinkedQueue() {
     
    head = tail = new Node<E>(null);
}

如上构造函数初始化时候会构建一个itemNULL的空节点作为链表的首尾节点。

三、offer操作

这个方法的作用就是在队列末端添加一个节点,如果传递的参数是null,就抛出空指针异常,否则由于该队列是无界队列,该方法会一直返回true,而且该方法使用CAS算法实现的,所以不会阻塞线程;

// 队列末端添加一个节点
public boolean offer(E e) {
     
	// 如果e为空,那么抛出空指针异常
    checkNotNull(e);
    // 将传进来的元素封装成一个节点,Node的构造器中调用UNSAFE.putObject(this, itemOffset, item)把e赋值给节点中的item
    final Node<E> newNode = new Node<E>(e);
	// [1] 这里的for循环从最后的节点开始
    for (Node<E> t = tail, p = t;;) {
     
        Node<E> q = p.next;
        // [2] 如果q为null,说明p就是最后的节点了
        if (q == null) {
     
            // [3] CAS更新:如果p节点的下一个节点是null,就把写个节点更新为newNode
            if (p.casNext(null, newNode)) {
     
                // [4] CAS成功,但是这时p==t,所以不会进入到这里的if里面,直接返回true
                // 那么什么时候会走到这里面来呢?其实是要有另外一个线程也在调用offer方法的时候,会进入到这里面来
                if (p != t) 
                    casTail(t, newNode); 
                return true;
            }
        }
        else if (p == q) // [5]
            p = (t != (t = tail)) ? t : head;
        else // [6]
            p = (p != t && t != (t = tail)) ? t : q;
    }
}

1、上面执行到[3]的时候,由于头节点和尾节点默认都是指向哨兵节点的,由于这个时候p的下一个节点为null,所以当前线程A执行CAS会成功,下图所示;

源码解读之(八)ConcurrentLinkedQueue_第4张图片

2、如果此时还有一个线程B也来尝试[3]CAS,由于此时p节点的下一个节点不是null了,于是线程B会跳到[1]出进行第二次循环,然后会到[6]中,由于pt此时是相等的,所以这里是false,即p=q,下图所示:

源码解读之(八)ConcurrentLinkedQueue_第5张图片

3、然后线程B又会跳到[1]处进行第三次循环,由于执行了Node q = p.next,所以此时q指向最后的null,就到了[3]处进行CAS,这次是可以成功的,成功之后如下图所示:

源码解读之(八)ConcurrentLinkedQueue_第6张图片

4、这个时候因为p!=t,所以可以进入到[4],这里又会进行一个CAS:如果tailt指向的节点一样,那么就将tail指向新添加的节点,如图所示,这个时候线程B也就执行完了;

源码解读之(八)ConcurrentLinkedQueue_第7张图片

5、其实还有[5]没有走到,这个是在poll操作之后才执行的,我们先跳过,等说完poll方法之后再回头看看;另外说一下,add方法其实就是调用的是offer方法,就不多说了;

public boolean add(E e) {
     
    return offer(e);
}

四、poll操作

这个方法是获取头部的这个节点,如果队列为空则返回null

public E poll() {
     
	// 这里其实就是一个goto的标记,用于跳出for循环
    restartFromHead:
    // [1]
    for (;;) {
     
        for (Node<E> h = head, p = h, q;;) {
     
            E item = p.item;
			// [2] 如果当前节点中存的值不为空,则CAS设置为null
            if (item != null && p.casItem(item, null)) {
     
                // [3] CAS成功就更新头节点的位置
                if (p != h) 
                    updateHead(h, ((q = p.next) != null) ? q : p);
                return item;
            }
            // [4] 当前队列为空,就返回null
            else if ((q = p.next) == null) {
     
                updateHead(h, p);
                return null;
            }
            // [5] 当前节点和下一个节点一样,说明节点自引用,则重新找头节点
            else if (p == q)
                continue restartFromHead;
            // [6] 
            else
                p = q;
        }
    }
}
final void updateHead(Node<E> h, Node<E> p) {
     
    if (h != p && casHead(h, p))
        h.lazySetNext(h);
}

分为几种情况:

第一种情况是线程A调用poll方法的时候,发现队列是空的,即头节点和尾节点都指向哨兵节点,就会直接到[4],返回null

第二种情况,线程A执行到了[4],此时有一个线程却调用offer方法添加了一个节点,下图所示,那么此时线程A就不会走[4]了,[5]也不满足,于是会到[6]这里来,然后线程A又会跳到[1]处进行循环,此时p指向的节点中item不为null,所以会到[2]中;

源码解读之(八)ConcurrentLinkedQueue_第8张图片

到了这里还没完,还记不记得offer方法中有一个地方的代码没有执行的啊!就是这种情况,尾节点自己引用自己,我们再调用offer会怎么样呢?

回到offer方法,先会到[1],然后q指向自己这个哨兵节点(注意,此时虽然p指向的节点中存的是null,但是p!=null},于是再到[5],此时的图如下左图所示;此时由于t==tail,所以p=head

源码解读之(八)ConcurrentLinkedQueue_第9张图片

再在offer方法循环一次,此时q指向null,下面左图所示,然后就可以进入[2]中进行CASCAS成功,因为此时p!=t,所以还要进行CAStail指向新节点,下面右图所示,可以让GC回收那个垃圾!

源码解读之(八)ConcurrentLinkedQueue_第10张图片

五、peek操作

这个方法的作用就是获取队列头部的元素,只获取不移除,注意这个方法和上面的poll方法的区别啊!

public E peek() {
     
	// [1] goto标志
    restartFromHead:
    for (;;) {
     
        for (Node<E> h = head, p = h, q;;) {
     
        	// [2]
            E item = p.item;
            // [3]
            if (item != null || (q = p.next) == null) {
     
                updateHead(h, p);
                return item;
            }
            // [4]
            else if (p == q)
                continue restartFromHead;
            // [5]
            else
                p = q;
        }
    }
}

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

1、如果队列中为空的时候,走到[3]的时候,就会如下图所示,由于h==p,所以updateHead方法啥也不做,然后返回就返回itemnull

源码解读之(八)ConcurrentLinkedQueue_第11张图片

2、如果队列不为空,那么如下左图所示,此时进入循环内不满足条件,会到[5]这里,将p指向q,然后再进行一次循环到[3],将q指向p的后一个节点,下面右图所示;

源码解读之(八)ConcurrentLinkedQueue_第12张图片

3、然后调用updateHead方法,用CAS将头节点指向p这里,然后将h自己指向自己,下图所示,最后返回item

源码解读之(八)ConcurrentLinkedQueue_第13张图片

六、size操作

获取当前队列元素个数,在并发环境下不是很有用,因为使用CAS没有加锁,所以从调用size函数到返回结果期间有可能增删元素,导致统计的元素个数不精确。

public int size() {
     
    int count = 0;
    for (Node<E> p = first(); p != null; p = succ(p))
        if (p.item != null)
            // 最大返回Integer.MAX_VALUE
            if (++count == Integer.MAX_VALUE)
                break;
    return count;
}

// 获取第一个队列元素(哨兵元素不算),没有则为null
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;
        }
    }
}

// 获取当前节点的next元素,如果是自引入节点则返回真正头节点
final Node<E> succ(Node<E> p) {
     
    Node<E> next = p.next;
    return (p == next) ? head : next;
}

七、remove操作

如果队列里面存在该元素则删除给元素,如果存在多个则删除第一个,并返回true,否者返回false

public boolean remove(Object o) {
     
	// 查找元素为空,直接返回false
    if (o != null) {
     
        Node<E> next, pred = null;
        for (Node<E> p = first(); p != null; pred = p, p = next) {
     
            boolean removed = false;
            E item = p.item;
            // 相等则使用cas值null,同时一个线程成功,失败的线程循环查找队列中其他元素是否有匹配的。
            if (item != null) {
     
                if (!o.equals(item)) {
     
                    // 获取next元素
                    next = succ(p);
                    continue;
                }
                removed = p.casItem(item, null);
            }

            next = succ(p);
            // 如果有前驱节点,并且next不为空则链接前驱节点到next,
            if (pred != null && next != null) 
                pred.casNext(p, next);
            if (removed)
                return true;
        }
    }
    return false;
}

八、ConcurrentLinkedQueue遇到的问题

这边debugger调试ConcurrentLinkedQueue遇到的一个坑!!!

测试主类:

public class ConcurrentLinkedQueueLocalTest {
     
    public static void main(String[] args) {
     
        ConcurrentLinkedQueueLocal queue = new ConcurrentLinkedQueueLocal();
        queue.offer(1);
        queue.offer(2);
    }
}

复制源码到本地并重命名为ConcurrentLinkedQueueLocal

其中第二个分支和第三个分支我加了日志打印输出

@Override
public boolean offer(E e) {
     
    checkNotNull(e);
    final Node<E> newNode = new Node<E>(e);

    for (Node<E> t = tail, p = t;;) {
     
        Node<E> q = p.next;
        if (q == null) {
     
            // p is last node
            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".
                if (p != t) // hop two nodes at a time
                    casTail(t, newNode);  // Failure is OK.
                return true;
            }
            // Lost CAS race to another thread; re-read next
        }
        else if (p == q) {
     
            System.out.println("第二次走了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.
            p = (t != (t = tail)) ? t : head;
        } else {
     
            System.out.println("第二次走了最后的else分支");
            // Check for tail updates after two hops.
            p = (p != t && t != (t = tail)) ? t : q;
        }
    }
}

下面问题来了!!!

1、直接run方式启动

第二次走了最后的else分支

2、debugger方式启动

第二次走了p==q分支

不信你可以自己调试下看看,很迷啊这操作!!!把我整了半天还是解决了。。。

3、解决方式

这是idea的坑,我们把这两个框不要选。
源码解读之(八)ConcurrentLinkedQueue_第14张图片
参考链接:http://www.nopapp.com/Ask/Question/1123839496278904832

代码测试地址:https://github.com/riemannChow/LeetCode/tree/master/src/main/java/com/test/ConcurrentLinkedQueueTest

九、总结

ConcurrentLinkedQueue使用CAS非阻塞算法实现,使用CAS解决了当前节点与next节点之间的安全链接和对当前节点值的赋值。由于使用CAS没有使用锁,所以获取size的时候有可能进行offerpoll或者remove操作,导致获取的元素个数不精确,所以在并发情况下size函数不是很有用。另外第一次peek或者first时候会把head指向第一个真正的队列元素。

下面总结下如何实现线程安全的,可知入队出队函数都是操作volatile变量:headtail。所以要保证队列线程安全只需要保证对这两个Node操作的可见性和原子性,由于volatile本身保证可见性,所以只需要看下多线程下如果保证对着两个变量操作的原子性。

对于offer操作是在tail后面添加元素,也就是调用tail.casNext方法,而这个方法是使用的CAS操作,只有一个线程会成功,然后失败的线程会循环一下,重新获取tail,然后执行casNext方法。对于poll也是这样的。

最后找的一张图方便帮助大家记忆:

源码解读之(八)ConcurrentLinkedQueue_第15张图片

你可能感兴趣的:(源码解读)