一.背景

  要实现对队列的安全访问,有两种方式:阻塞算法和非阻塞算法。阻塞算法的实现是使用一把锁(出队和入队同一把锁ArrayBlockingQueue)和两把锁(出队和入队各一把锁LinkedBlockingQueue)来实现;非阻塞算法使用自旋+CAS实现。

  今天来探究下使用非阻塞算法来实现的线程安全队列ConcurrentLinkedQueue,它是一个基于链接节点的无界线程安全队列,采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。它采用了“wait-free”算法(即CAS算法)来实现。

  ConcurrentLinkedQueue的类图结构:

  从类图中可以看到,ConcurrentLinkedQueue由head和tail节点组成,每个节点Node由节点元素item和指向下一个节点的引用next组成,节点与节点之间通过next关联起来组成一张链表结构的队列。

  二.源码解析

  1. 构造方法

        private static class Node<E> {
    volatile E item;//元素
    volatile Node<E> next;//下一节点 Node(E item) {//添加元素
    UNSAFE.putObject(this, itemOffset, item);
    } boolean casItem(E cmp, E val) {//cas修改元素
    return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
    } void lazySetNext(Node<E> val) {//添加节点
    UNSAFE.putOrderedObject(this, nextOffset, val);
    } boolean casNext(Node<E> cmp, Node<E> val) {//cas修改节点
    return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
    } private static final sun.misc.Unsafe UNSAFE;
    private static final long itemOffset;
    private static final long nextOffset; static {
    try {
    UNSAFE = sun.misc.Unsafe.getUnsafe();
    Class<?> k = Node.class;
    //获得元素的偏移位置
    itemOffset = UNSAFE.objectFieldOffset
    (k.getDeclaredField("item"));
    //获得下一节点的偏移位置
    nextOffset = UNSAFE.objectFieldOffset
    (k.getDeclaredField("next"));
    } catch (Exception e) {
    throw new Error(e);
    }
    }
    }
    //头节点
    private transient volatile Node<E> head;
    //尾节点
    private transient volatile Node<E> tail;
    public ConcurrentLinkedQueue() {
    //默认情况下head节点存储的元素为空,tail节点等于head节点。
    head = tail = new Node<E>(null);
    }
    public ConcurrentLinkedQueue(Collection<? extends E> c) {
    Node<E> h = null, t = null;
    //遍历集合
    for (E e : c) {
    checkNotNull(e);//检查是否为空,如果为空抛出空指针异常
    //创建节点和将元素存储到节点中
    Node<E> newNode = new Node<E>(e);
    if (h == null)//头节点为空
    h = t = newNode;//头和尾节点是创建的节点
    else {
    t.lazySetNext(newNode);//添加节点
    t = newNode;//修改尾节点的标识
    }
    }
    //如果集合没有元素,设置队列的头尾节点为空
    if (h == null)
    h = t = new Node<E>(null);
    head = h;//更新队列的头节点标识
    tail = t;//更新队列的尾节点标识
    }
    private static void checkNotNull(Object v) {
    if (v == null)
    throw new NullPointerException();
    }
  2. 入队add:

    • 入队操作主要做两件事情,第一是将入队节点设置成当前队列尾节点的下一个节点;第二是更新tail节点,如果tail节点的next节点不为空,则将入队节点设置成tail节点,如果tail节点的next节点为空,则将入队节点设置成tail的next节点,所以tail节点不总是尾节点

    • 上面的分析让我们从单线程入队的角度来理解入队过程,但是多个线程同时进行入队情况就变得更加复杂,因为可能会出现其他线程插队的情况。如果有一个线程正在入队,那么它必须先获取尾节点,然后设置尾节点的下一个节点为入队节点,但这时可能有另外一个线程插队了,那么队列的尾节点就会发生变化,这时当前线程要暂停入队操作,然后重新获取尾节点。

    • 源码解析:从下面可以看出,入队永远是返回true,所以不要通过返回值判断是否入队成功

      public boolean add(E e) {
      return offer(e);
      }
      public boolean offer(E e) {
      checkNotNull(e);//检查是否为空
      //创建入队节点,将元素添加到节点中
      final Node<E> newNode = new Node<E>(e);
      //自旋队列CAS直到入队成功
      // 1、根据tail节点定位出尾节点(last node);2、将新节点置为尾节点的下一个节点;3、casTail更新尾节点
      for (Node<E> t = tail, p = t;;) {
      //p是尾节点,q得到尾节点的next
      Node<E> q = p.next;
      //如果q为空
      if (q == null) {
      //p是last node,将尾节点的next修改为创建的节点
      if (p.casNext(null, newNode)) {
      //p在遍历后会变化,因此需要判断,如果不相等即p != t = tail,表示t(= tail)不是尾节点,则将入队节点设置成tail节点,更新失败了也没关系,因为失败了表示有其他线程成功更新了tail节点
      if (p != t)
      casTail(t, newNode);//入队节点更新为尾节点,允许失败,因此t= tail并不总是尾节点
      return true;//结束
      }
      }
      //重新获取head节点:多线程操作时,轮询后p有可能等于q,此时,就需要对p重新赋值
      //(多线程自引用的情况,只有offer()和poll()交替执行时会出现)
      else if (p == q)
      //因为并发下可能tail被改了,如果被改了,则使用新的t,否则跳转到head,从链表头重新轮询,因为从head开始所有的节点都可达
      p = (t != (t = tail)) ? t : head;//运行到这里再继续自旋遍历
      else
      /**
      * 寻找尾节点,同样,当t不等于p时,说明p在上面被重新赋值了,并且tail也被别的线程改了,则使用新的tail,否则循环检查p的下个节点
      * (多offer()情况下会出现)
      * p=condition?result1:result2
      * 满足result1的场景为 :
      * 获取尾节点tail的快照已经过时了(其他线程更新了新的尾节点tail),直接跳转到当前获得的最新尾节点的地方
      * 满足result2的场景为:
      * 多线程同时操作offer(),执行p.casNext(null, newNode)CAS成功后,未更新尾节点(未执行casTail(t, newNode)方法:两种原因 1是未满足前置条件if判断 2是CAS更新失败),直接找next节点
      */
      p = (p != t && t != (t = tail)) ? t : q;//运行到这里再继续自旋遍历
      }
      }
    1. debug断点测试案例:

      public static void main(String[] args) throws IndexOutOfBoundsException {
      ConcurrentLinkedQueue c = new ConcurrentLinkedQueue();
      new Thread(()->{
      int i;
      for(i=0;i<10;){
      c.offer(i++);
      Object poll = c.poll();//注释或取消进行测试
      System.out.println(Thread.currentThread().getName()+":"+poll);
      }
      }).start();
      new Thread(()->{
      int i;
      for(i=200;i<210;){
      c.offer(i++);
      Object poll = c.poll();//注释或取消进行测试
      System.out.println(Thread.currentThread().getName()+":"+poll);
      }
      }).start();
      }
    2. tail多线程的更新情况:通过p和t是否相等来判断

  3. 出队poll:

    • 从上图可知,并不是每次出队时都更新head节点,当head节点里有元素时,直接弹出head节点里的元素,而不会更新head节点。只有当head节点里没有元素时,出队操作才会更新head节点。采用这种方式也是为了减少使用CAS更新head节点的消耗,从而提高出队效率。
    • 源码解析:

      public E poll() {
      restartFromHead:
      //自旋
      for (;;) {
      //获得头节点
      for (Node<E> h = head, p = h, q;;) {
      E item = p.item;//获得头节点元素
      //如果头节点元素不为null并且cas删除头节点元素成功
      if (item != null && p.casItem(item, null)) {
      //p被修改了
      if (p != h) // hop two nodes at a time
      // 如果p 的next 属性不是null ,将 p 作为头节点,而 q 将会消失
      updateHead(h, ((q = p.next) != null) ? q : p);
      return item;
      }
      //如果头节点的元素为空或头节点发生了变化,这说明头节点已经被另外一个线程修改了。
      // 那么获取p节点的下一个节点,如果p节点的下一节点为null,则表明队列已经空了
      // 如果 p(head) 的 next 节点 q 也是null,则表示没有数据了,返回null,则将 head 设置为null
      // 注意:updateHead 方法最后还会将原有的 head 作为自己 next 节点,方便offer 连接。
      else if ((q = p.next) == null) {
      updateHead(h, p);
      return null;
      }
      //如果 p == q,说明别的线程取出了 head,并将 head 更新了。就需要重新开始获取head节点
      else if (p == q)
      continue restartFromHead;
      // 如果下一个元素不为空,则将头节点的下一个节点设置成头节点
      else
      p = q;
      }
      }
      }
      final void updateHead(Node<E> h, Node<E> p) {
      if (h != p && casHead(h, p))
      // 将旧的头结点h的next域指向为h
      h.lazySetNext(h);
      }
  4. 入队和出队操作中,都有p == q的情况,在下面这种情况中:

    • 在弹出一个节点之后,tail节点有一条指向自己的虚线,这是什么意思呢?在poll()方法中,移除元素之后,会调用updateHead方法,其中有h.lazySetNext(h),可以看到,在更新完head之后,会将旧的头结点h的next域指向为h,上图中所示的虚线也就表示这个节点的自引用。
    • 如果这时,再有一个线程来添加元素,通过tail获取的next节点则仍然是它本身,这就出现了p == q的情况,出现该种情况之后,则会触发执行head的更新,将p节点重新指向为head,所有“活着”的节点(指未删除节点),都能从head通过遍历可达,这样就能通过head成功获取到尾节点,然后添加元素了。

  5. 获取首部元素peek:

    • 从图中可以看到,peek操作会改变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;
      }
      /如果 p == q,说明别的线程取出了 head,并将 head 更新了。就需要重新开始获取head节点
      else if (p == q)
      continue restartFromHead;
      // 如果下一个元素不为空,则将头节点的下一个节点设置成头节点
      else
      p = q;
      }
      }
      }
  6. 判断队列是否为空isEmpty:

        public boolean isEmpty() {
    return first() == 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;//头节点有值返回节点,否则返回null
    }
    else if (p == q)
    continue restartFromHead;
    else
    p = q;
    }
    }
    }
  7. 获取个数size:在并发环境中,其结果可能不精确,因为整个过程都没有加锁,所以从调用size方法到返回结果期间有可能增删元素,导致统计的元素个数不精确。

        public int size() {
    int count = 0;
    // first()获取第一个具有非空元素的节点,若不存在,返回null
    // succ(p)方法获取p的后继节点,若p == p的后继节点,则返回head
    for (Node<E> p = first(); p != null; p = succ(p))
    //节点有元素数量+1
    if (p.item != null)
    if (++count == Integer.MAX_VALUE)
    break;
    return count;
    }
    //取下一节点
    final Node<E> succ(Node<E> p) {
    Node<E> next = p.next;
    //若p == p的后继节点(自引用情况下会出现),则返回head
    return (p == next) ? head : next;
    }
  8. 判断元素是否包含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;
    // 若找到匹配节点,则返回true
    if (item != null && o.equals(item))
    return true;
    }
    return false;
    }
  9. 删除元素remove:

        public boolean remove(Object o) {
    //删除的元素不能为null,
    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;//节点元素
    if (item != null) {
    //节点的元素不等于要删除的元素,获取下一节点进行遍历循环操作
    if (!o.equals(item)) {
    next = succ(p);//将当前遍历的节点移到下一节点
    continue;
    }
    //节点元素等于删除元素,CAS将节点元素置为null
    removed = p.casItem(item, null);
    }
    next = succ(p);//获取删除节点的下一节点,
    //有前节点和后置节点
    if (pred != null && next != null) // unlink
    pred.casNext(p, next);//删除当前节点,即当前节点移除出队列
    if (removed)//元素删除了返回true
    return true;
    }
    }
    return false;
    }

  三.总结

  • 使用 CAS 原子指令来处理对数据的并发访问,这是非阻塞算法得以实现的基础。
  • head/tail 并非总是指向队列的头 / 尾节点,也就是说允许队列处于不一致状态。 这个特性把入队 / 出队时,原本需要一起原子化执行的两个步骤分离开来,从而缩小了入队 / 出队时需要原子化更新值的范围到唯一变量。这是非阻塞算法得以实现的关键。
  • 由于队列有时会处于不一致状态。为此,ConcurrentLinkedQueue 使用三个不变式来维护非阻塞算法的正确性。
  • 以批处理方式来更新 head/tail,从整体上减少入队 / 出队操作的开销。
  • 为了有利于垃圾收集,队列使用特有的 head 更新机制;为了确保从已删除节点向后遍历,可到达所有的非删除节点,队列使用了特有的向后推进策略。

  四.参考

  • https://blog.csdn.net/qq_38293564/article/details/80798310
  • https://www.ibm.com/developerworks/cn/java/j-lo-concurrent/index.html

多线程高并发编程(11) -- 非阻塞队列ConcurrentLinkedQueue源码分析的更多相关文章

  1. Java并发容器之非阻塞队列ConcurrentLinkedQueue

    参考资料:http://blog.csdn.net/chenchaofuck1/article/details/51660521 实现一个线程安全的队列有两种实现方式:一种是使用阻塞算法,阻塞队列就是 ...

  2. Java并发编程笔记之读写锁 ReentrantReadWriteLock 源码分析

    我们知道在解决线程安全问题上使用 ReentrantLock 就可以,但是 ReentrantLock 是独占锁,同时只有一个线程可以获取该锁,而实际情况下会有写少读多的场景,显然 Reentrant ...

  3. Java并发编程笔记之 CountDownLatch闭锁的源码分析

    JUC 中倒数计数器 CountDownLatch 的使用与原理分析,当需要等待多个线程执行完毕后在做一件事情时候 CountDownLatch 是比调用线程的 join 方法更好的选择,CountD ...

  4. Java 多线程高并发编程 笔记(一)

    本篇文章主要是总结Java多线程/高并发编程的知识点,由浅入深,仅作自己的学习笔记,部分侵删. 一 . 基础知识点 1. 进程于线程的概念 2.线程创建的两种方式 注:public void run( ...

  5. 多线程高并发编程(3) -- ReentrantLock源码分析AQS

    背景: AbstractQueuedSynchronizer(AQS) public abstract class AbstractQueuedSynchronizer extends Abstrac ...

  6. Java并发包源码学习系列:基于CAS非阻塞并发队列ConcurrentLinkedQueue源码解析

    目录 非阻塞并发队列ConcurrentLinkedQueue概述 结构组成 基本不变式 head的不变式与可变式 tail的不变式与可变式 offer操作 源码解析 图解offer操作 JDK1.6 ...

  7. 多线程高并发编程(8) -- Fork/Join源码分析

    一.概念 Fork/Join就是将一个大任务分解(fork)成许多个独立的小任务,然后多线程并行去处理这些小任务,每个小任务处理完得到结果再进行合并(join)得到最终的结果. 流程:任务继承Recu ...

  8. 多线程高并发编程(10) -- ConcurrentHashMap源码分析

    一.背景 前文讲了HashMap的源码分析,从中可以看到下面的问题: HashMap的put/remove方法不是线程安全的,如果在多线程并发环境下,使用synchronized进行加锁,会导致效率低 ...

  9. 9.并发包非阻塞队列ConcurrentLinkedQueue

    jdk1.7.0_79  队列是一种非常常用的数据结构,一进一出,先进先出. 在Java并发包中提供了两种类型的队列,非阻塞队列与阻塞队列,当然它们都是线程安全的,无需担心在多线程并发环境所带来的不可 ...

随机推荐

  1. 解决linux下启动tomcat找不到jdk

    在tomcat目录下 vim catalina.sh 头部加入 JAVA_HOME='/root/use/local/java/jdk/';export JAVA_HOME;

  2. [jQuery插件]手写一个图片懒加载实现

    教你做图片懒加载插件 那一年 那一年,我还年轻 刚接手一个ASP.NET MVC 的 web 项目, (C#/jQuery/Bootstrap) 并没有做 web 的经验,没有预留学习时间, (作为项 ...

  3. 使用Docker发布blazor wasm

    Blazor编译后的文件是静态文件,所以我们只需要一个支持静态页面的web server即可. 根据不同项目,会用不同的容器编排,本文已无网关的情况下为例,一步一步展示如何打包进docker 需求 H ...

  4. [Python进阶]002.装饰器(1)

    装饰器(1) 介绍 HelloWorld 需求 使用函数式编程 加入装饰器 解析 介绍 Python的装饰器叫Decorator,就是对一个模块做装饰. 作用: 为已存在的对象添加额外功能. 与Jav ...

  5. [JavaWeb基础] 012.Struts2 自定义标签使用

    在做开发中,我们会把一些比较经常使用到的代码封装起来,这样可以加快开发的速度和减少错误,并且在修改bug可以一次修改多次修复.那么在前端页面上,如果我们要经常用到公用的显示功能,并涉及到服务端逻辑操作 ...

  6. Java IO(四) InputStream 和 OutputStream

    Java IO(四) InputStream 和 OutputStream 一.介绍 InputStream 和 OutputStream 是字节流的超类(父类),都是抽象类,都是通过实例化它们的子类 ...

  7. 解决google play上架App设置隐私政策声明问题

    在我们的app上架到google play后,为了赚点小钱,就集成google ads,然而这会引发一个新的问题,那就是设置隐私政策声明的问题,通常我们会收到一封来自google play的邮件,提示 ...

  8. 用TensorFlow搭建一个万能的神经网络框架(持续更新)

    我一直觉得TensorFlow的深度神经网络代码非常困难且繁琐,对TensorFlow搭建模型也十分困惑,所以我近期阅读了大量的神经网络代码,终于找到了搭建神经网络的规律,各位要是觉得我的文章对你有帮 ...

  9. kubeadm实现k8s高可用集群环境部署与配置

    高可用架构 k8s集群的高可用实际是k8s各核心组件的高可用,这里使用主备模式,架构如下: 主备模式高可用架构说明: 核心组件 高可用模式 高可用实现方式 apiserver 主备 keepalive ...

  10. Rocket - debug - Example: Write Memory

    https://mp.weixin.qq.com/s/on1LugO9fTFJstMes3T2Xg 介绍riscv-debug的使用实例:使用三种方法写内存. 1. Using System Bus ...