三十五、并发编程之并发容器ConcurrentLinkedQueue原理与使用

一、ConcurrentLinkedQueue简介

一个基于链接节点的无界线程安全队列。此队列按照 FIFO(先进先出)原则对元素进行排序。队列的头部是队列中时间最长的元素。队列的尾部是队列中时间最短的元素。

新的元素插入到队列的尾部,队列获取操作从队列头部获得元素。当多个线程共享访问一个公共collection 时,ConcurrentLinkedQueue 是一个恰当的选择。此队列不允许使用 null 元素。

二、 ConcurrentLinkedQueue类图结构

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

public ConcurrentLinkedQueue() {
    head = tail = new Node<E>(null);
}

如上代码初始化时候会构建一个item为NULL的空节点作为链表的首尾节点。

三、方法摘要

  • offer(E e) 将指定元素插入此队列的尾部。
  • poll() 获取并移除此队列的头,如果此队列为空,则返回 null。
	public static void main(String[] args) {
        ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue();
        queue.offer("哈哈哈");
        System.out.println("offer后,队列是否空?" + queue.isEmpty());
        System.out.println("从队列中poll:" + queue.poll());
        System.out.println("poll后,队列是否空?" + queue.isEmpty());
    }

offer是往队列添加元素,poll是从队列取出元素并且删除该元素。
执行结果:

offer后,队列是否空?false
从队列中poll:哈哈哈
poll后,队列是否空?true

ConcurrentLinkedQueue中的add() 和 offer() 完全一样,都是往队列尾部添加元素。

  • peek() 获取但不移除此队列的头;如果此队列为空,则返回 null
	public static void main(String[] args) {
        ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue();
        queue.offer("哈哈哈");
        System.out.println("offer后,队列是否空?" + queue.isEmpty());
        System.out.println("从队列中peek:" + queue.peek());
        System.out.println("从队列中peek:" + queue.peek());
        System.out.println("从队列中peek:" + queue.peek());
        System.out.println("peek后,队列是否空?" + queue.isEmpty());
    }

执行结果:

offer后,队列是否空?false
从队列中peek:哈哈哈
从队列中peek:哈哈哈
从队列中peek:哈哈哈
poll后,队列是否空?false
  • remove(Object o) 从队列中移除指定元素的单个实例(如果存在)
	public static void main(String[] args) {
        ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue();
        queue.offer("哈哈哈");
        System.out.println("offer后,队列是否空?" + queue.isEmpty());
        System.out.println("从队列中remove已存在元素 :" + queue.remove("哈哈哈"));
        System.out.println("从队列中remove不存在元素:" + queue.remove("123"));
        System.out.println("remove后,队列是否空?" + queue.isEmpty());
    }

remove一个已存在元素,会返回true,remove不存在元素,返回false
执行结果:

offer后,队列是否空?false
从队列中remove已存在元素 :true
从队列中remove不存在元素:false
remove后,队列是否空?true
  • size() 返回此队列中的元素数量

注意:

  1. 如果此队列包含的元素数大于 Integer.MAX_VALUE,则返回 Integer.MAX_VALUE。
  2. 需要小心的是,与大多数 collection 不同,此方法不是一个固定时间操作。由于这些队列的异步特性,确定当前的元素数需要进行一次花费 O(n) 时间的遍历。
  3. 所以在需要判断队列是否为空时,尽量不要用 queue.size()>0,而是用 !queue.isEmpty()

比较size()和isEmpty() 效率的示例:
场景:10000个人去饭店吃饭,10张桌子供饭,分别比较size() 和 isEmpty() 的耗时

public class Test01ConcurrentLinkedQueue {
    public static void main(String[] args) throws InterruptedException {
        int peopleNum = 10000;//吃饭人数
        int tableNum = 10;//饭桌数量

        ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
        CountDownLatch count = new CountDownLatch(tableNum);//计数器

        //将吃饭人数放入队列(吃饭的人进行排队)
        for(int i=1;i<=peopleNum;i++){
            queue.offer("消费者_" + i);
        }
        //执行10个线程从队列取出元素(10个桌子开始供饭)
        System.out.println("-----------------------------------开饭了-----------------------------------");
        long start = System.currentTimeMillis();
        ExecutorService executorService = Executors.newFixedThreadPool(tableNum);
        for(int i=0;i<tableNum;i++) {
            executorService.submit(new Dinner("00" + (i+1), queue, count));
        }
        //计数器等待,知道队列为空(所有人吃完)
        count.await();
        long time = System.currentTimeMillis() - start;
        System.out.println("-----------------------------------所有人已经吃完-----------------------------------");
        System.out.println("共耗时:" + time);
        //停止线程池
        executorService.shutdown();
    }

    private static class Dinner implements Runnable{
        private String name;
        private ConcurrentLinkedQueue<String> queue;
        private CountDownLatch count;

        public Dinner(String name, ConcurrentLinkedQueue<String> queue, CountDownLatch count) {
            this.name = name;
            this.queue = queue;
            this.count = count;
        }

        @Override
        public void run() {
            //while (queue.size() > 0){
            while (!queue.isEmpty()){
                //从队列取出一个元素 排队的人少一个
                System.out.println("【" +queue.poll() + "】----已吃完..., 饭桌编号:" + name);
            }
            count.countDown();//计数器-1
        }
    }
}

执行结果:
使用size耗时:757ms
在这里插入图片描述

使用isEmpty耗时:210
在这里插入图片描述

当数据量越大,这种耗时差距越明显。所以这种判断用isEmpty 更加合理

  • contains(Object o) 如果此队列包含指定元素,则返回 true
	public static void main(String[] args) throws InterruptedException {
        ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue();
        queue.offer("123");
        System.out.println(queue.contains("123"));
        System.out.println(queue.contains("234"));
    }

执行结果:
在这里插入图片描述

  • toArray() 返回以恰当顺序包含此队列所有元素的数组
  • toArray(T[] a) 返回以恰当顺序包含此队列所有元素的数组;返回数组的运行时类型是指定数组的运行时类型
	public static void main(String[] args) throws InterruptedException {
        ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<String>();
        queue.offer("123");
        queue.offer("234");
        Object[] objects = queue.toArray();
        System.out.println(objects[0] + ", " + objects[1]);

        //将数据存储到指定数组
        String[] strs = new String[2];
        queue.toArray(strs);
        System.out.println(strs[0] + ", " + strs[1]);
    }

执行结果:
在这里插入图片描述

  • iterator() 返回在此队列元素上以恰当顺序进行迭代的迭代器
	public static void main(String[] args) throws InterruptedException {
        ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<String>();
        queue.offer("123");
        queue.offer("234");
        Iterator<String> iterator = queue.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }

三、源码详解

1、offer操作

offer操作是在链表末尾添加一个元素,下面看看实现原理。

public boolean offer(E e) {
    //e为null则抛出空指针异常
    checkNotNull(e);  
     
   //构造Node节点构造函数内部调用unsafe.putObject,后面统一讲
    final Node<E> newNode = new Node<E>(e);     
    //从尾节点插入
    for (Node<E> t = tail, p = t;;) {
        Node<E> q = p.next;
        //如果q=null说明p是尾节点则插入
        if (q == null) {
           	//cas插入(1)
            if (p.casNext(null, newNode)) {
                //cas成功说明新增节点已经被放入链表,然后设置当前尾节点(包含head,1,3,5.。。个节点为尾节点)
                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)//(2)
            //多线程操作时候,由于poll时候会把老的head变为自引用,然后head的next变为新head,所以这里需要
            //重新找新的head,因为新的head后面的节点才是激活的节点
            p = (t != (t = tail)) ? t : head;
        else
            // 寻找尾节点(3)
            p = (p != t && t != (t = tail)) ? t : q;
    }
}

从构造函数知道一开始有个item为null的哨兵节点,并且head和tail都是指向这个节点,然后当一个线程调用offer时候首先
在这里插入图片描述

如图首先查找尾节点,qnull,p就是尾节点,所以执行p.casNext通过cas设置p的next为新增节点,这时候pt所以不重新设置尾节点为当前新节点。由于多线程可以调用offer方法,所以可能两个线程同时执行到了(1)进行cas,那么只有一个会成功(假如线程1成功了),成功后的链表为:
在这里插入图片描述

失败的线程会循环一次这时候指针为:
在这里插入图片描述
这时候会执行(3)所以p=q,然后在循环后指针位置为:
在这里插入图片描述
所以没有其他线程干扰的情况下会执行(1)执行cas把新增节点插入到尾部,没有干扰的情况下线程2 cas会成功,然后去更新尾节点tail,由于p!=t所以更新。这时候链表和指针为:
在这里插入图片描述

假如线程2cas时候线程3也在执行,那么线程3会失败,循环一次后,线程3的节点状态为:
在这里插入图片描述
这时候p!=t ;并且t的原始值为told,t的新值为tnew ,所以told!=tnew,所以 p=tnew=tail;
然后在循环一下后节点状态:
在这里插入图片描述
q=null所以执行(1)。
现在就差p=q这个分支还没有走,这个要在执行poll操作后才会出现这个情况。poll后会存在下面的状态
在这里插入图片描述
这个时候添加元素时候指针分布为:
在这里插入图片描述
所以会执行(2)分支 结果 p=head
然后循环,循环后指针分布:
在这里插入图片描述
所以执行(1),然后p!=t所以设置tail节点。现在分布图:
在这里插入图片描述
自引用的节点会被垃圾回收掉。

2、add操作

add操作是在链表末尾添加一个元素,下面看看实现原理。
其实内部调用的还是offer

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

3、poll操作

poll操作是在链表头部获取并且移除一个元素,下面看看实现原理。

public E poll() {
    restartFromHead:
    //死循环
    for (;;) {
        //死循环
        for (Node<E> h = head, p = h, q;;) {
            //保存当前节点值
            E item = p.item;
            //当前节点有值则cas变为null(1)
            if (item != null && p.casItem(item, null)) {
                //cas成功标志当前节点以及从链表中移除
                if (p != h) // 类似tail间隔2设置一次头节点(2)
                    updateHead(h, ((q = p.next) != null) ? q : p);
                return item;
            }
            //当前队列为空则返回null(3)
            else if ((q = p.next) == null) {
                updateHead(h, p);
                return null;
            }
            //自引用了,则重新找新的队列头节点(4)
            else if (p == q)
                continue restartFromHead;
            else//(5)
                p = q;
        }
    }
}
    final void updateHead(Node<E> h, Node<E> p) {
        if (h != p && casHead(h, p))
            h.lazySetNext(h);
    }

当队列为空时候:
在这里插入图片描述

可知执行(3)这时候有两种情况,第一没有其他线程添加元素时候(3)结果为true然后因为h!=p为false所以直接返回null。第二在执行q=p.next前,其他线程已经添加了一个元素到队列,这时候(3)返回false,然后执行(5)p=q,然后循环后节点分布:
在这里插入图片描述
这时候执行(1)分支,进行cas把当前节点值值为null,同时只有一个线程会成功,cas成功 标示该节点从队列中移除了,然后p!=h,调用updateHead方法,参数为h,p;h!=p所以把p变为当前链表head节点,然后h节点的next指向自己。现在状态为:
在这里插入图片描述
cas失败后 会再次循环,这时候分布图为:
在这里插入图片描述
这时候执行(3)返回null.
现在还有个分支(4)没有执行过,那么什么时候会执行那?
在这里插入图片描述
这时候执行(1)分支,进行cas把当前节点值值为null,同时只有一个线程A会成功,cas成功 标示该节点从队列中移除了,然后p!=h,调用updateHead方法,假如执行updateHead前另外一个线程B开始poll这时候它p指向为原来的head节点,然后当前线程A执行updateHead这时候B线程链表状态为:
在这里插入图片描述
所以会执行(4)重新跳到外层循环,获取当前head,现在状态为:
在这里插入图片描述

4、peek操作

peek操作是获取链表头部一个元素(只读取不移除),下面看看实现原理。
代码与poll类似,只是少了castItem.并且peek操作会改变head指向,offer后head指向哨兵节点,第一次peek后head会指向第一个真的节点元素。

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

5、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;
}

6、remove操作

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

public boolean remove(Object o) {
    //查找元素为空,直接返回false
    if (o == null) return false;
    Node<E> pred = null;
    for (Node<E> p = first(); p != null; p = succ(p)) {
        E item = p.item;

        //相等则使用cas值null,同时一个线程成功,失败的线程循环查找队列中其他元素是否有匹配的。
        if (item != null &&
            o.equals(item) &&
            p.casItem(item, null)) {

            //获取next元素
            Node<E> next = succ(p);

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

7、contains操作

判断队列里面是否含有指定对象,由于是遍历整个队列,所以类似size 不是那么精确,有可能调用该方法时候元素还在队列里面,但是遍历过程中才把该元素删除了,那么就会返回false.

public boolean contains(Object o) {
    if (o == null) return false;
    for (Node<E> p = first(); p != null; p = succ(p)) {
        E item = p.item;
        if (item != null && o.equals(item))
            return true;
    }
    return false;
}

四、总结

ConcurrentLinkedQueue使用CAS非阻塞算法实现使用CAS解决了当前节点与next节点之间的安全链接和对当前节点值的赋值。由于使用CAS没有使用锁,所以获取size的时候有可能进行offer,poll或者remove操作,导致获取的元素个数不精确,所以在并发情况下size函数不是很有用。另外第一次peek或者first时候会把head指向第一个真正的队列元素。
下面总结下如何实现线程安全的,可知入队出队函数都是操作volatile变量:head,tail。所以要保证队列线程安全只需要保证对这两个Node操作的可见性和原子性,由于volatile本身保证可见性,所以只需要看下多线程下如果保证对着两个变量操作的原子性。
对于offer操作是在tail后面添加元素,也就是调用tail.casNext方法,而这个方法是使用的CAS操作,只有一个线程会成功,然后失败的线程会循环一下,重新获取tail,然后执行casNext方法。对于poll也是这样的。

原文1
原文2

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值