ConcurrentLinkedQueue源码


JAVA后端开发知识总结(持续更新…)


ConcurrentLinkedQueue源码解析



一、ConcurrentLinkedQueue简介及基本结构

  ConcurrentLinkedQueue是可以用来实现高并发的无界线程安全队列,底层是一个单向链表。它采用非阻塞算法,通过循环CAS保证了入队、出队等操作的线程安全。

  ConcurrentLinkedQueue中的Node节点如下所示,其item和next属性都是多线程可见的(通过volatile关键字)。对Node的操作,不论是设置item值还是设置next,都是通过CAS算法保证线程安全。对于compareAndSwapObject()方法,cmp表示期望值,val表示设置目标值,只有当前值和cmp相等时,才能设置成功。

// Node节点
private static class Node<E> {
        volatile E item;
        volatile Node<E> next;
        // CAS设置item
        boolean casItem(E cmp, E val) {
            return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
        }
        // CAS设置next
        boolean casNext(Node<E> cmp, Node<E> val) {
            return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
        }
}

  对于ConcurrentLinkedQueue底层链表,有一个头结点head和尾节点tail用于进行链表队列的管理。

// 头结点
private transient volatile Node<E> head;
// 尾节点
private transient volatile Node<E> tail;
// 默认构造函数,头结点和尾节点初始化在一起
public ConcurrentLinkedQueue() {
        head = tail = new Node<E>(null);
}

二、ConcurrentLinkedQueue的常用方法解析

2.1 入队方法offer()

入队方法offer()采用尾插法,没有任何锁操作,线程安全完全由CAS和队列算法保证。

public boolean offer(E e) {
        checkNotNull(e);
        final Node<E> newNode = new Node<E>(e);
        // 大CAS,不成功就会一直循环
        for (Node<E> t = tail, p = t;;) {
            Node<E> q = p.next;
            
            // p是最后一个节点的情况
            if (q == null) {
                // 直接进行 CAS 尾插
                if (p.casNext(null, newNode)) {
                    // 每两次更新一下 tail
                    if (p != t) 
                        casTail(t, newNode);  
                    return true;
                }
                // CAS如果不成功,会循环尝试
            }
            
	    // 处理哨兵节点情况
            else if (p == q)
                p = (t != (t = tail)) ? t : head;
                
            // p不是最后一个节点的情况
            else
                // 在两次添加之后检查尾部更新,即更新 p节点指向尾节点
                // 不是原子操作,代表取下一个或最后一个节点
                p = (p != t && t != (t = tail)) ? t : q;
        }
}

总结

  1. offer()的主要任务一是将入队节点进行线程安全地尾插,二是更新tail节点。
  2. 对于tail节点的更新,是存在拖延现象的,它并不总是在更新,事实上,tail节点每两次入队后更新一次(可以总结为入队的是偶数个节点时,才会更新tail)。
  3. for循环是核心,没有出口,直到CAS尝试成功。
  4. 如果 tail是指向尾节点的 (q == null),直接进入第一个分支,然后采用 CAS 实现新节点的尾插。此时(p == t)是成立的,因此 不会进行tail的更新
  5. 如果 tail不是指向尾节点的,会先进入最后一个分支,开始查找最后一个元素的位置。此时p会更新为指向最后一个元素。在第二次循环时,会进入第一个分支(q = p.next == null),但由于此时p的指向和tail不是一个节点,会利用 CAS 更新tail节点为最后一个节点
  6. 对于 第三个分支的更新操作 ,主要是考虑到了多线程时的线程安全更新问题。由于 != 不是原子操作,所以在并发比较的过程中,可能出现中断问题,导致尾节点先被其它线程更新了。即在获得左边的t后,右边的被其它线程修改(t != t成立)。此时由于尾节点tail被篡改,则以新的tail为尾节点
  7. 第二个分支用于处理哨兵节点情况(即next指向了自己),它主要是已经被删除的节点,因为无法从next获取下一个节点,直接返回head从头遍历。但是也考虑了线程安全问题,中途如果tail被改变(t != t),直接使用新的tail。哨兵节点的产生在poll()方法中介绍。

  如果每次都使用循环CAS更新tail节点,影响性能,所以采用延迟更新的方式减少CAS更新tail节点的次数,提高入队的效率。下图是入队的基本演示(图片来源于此):

在这里插入图片描述

2.2 出队方法poll()

出队方法poll()采用头删法,会产生哨兵节点图片来源于此

在这里插入图片描述

public E poll() {
        restartFromHead:
        for (;;) {
            // p节点指向需要出队的节点
            for (Node<E> h = head, p = h, q;;) {
                E item = p.item;
                // 如果 p 不为 null,则通过 CAS 来循环设置 p 指向的元素为null,成功则返回删除的元素值
                if (item != null && p.casItem(item, null)) {
                    // 每两次更新 head 指向
                    if (p != h) 
                        updateHead(h, ((q = p.next) != null) ? q : p);
                    return item;
                }
                // 如果第一个节点(p.next)的元素为空,则已被其它线程修改
                // 直接更新头结点
                else if ((q = p.next) == null) {
                    updateHead(h, p);
                    return null;
                }
                // 遇到哨兵节点,直接从新的 head 开始查找
                else if (p == q)
                    continue restartFromHead;
                // head 的 item 为 null,p 指向 p.next,即第一个元素
                else
                    p = q;
            }
        }
}

// 更新头节点
final void updateHead(Node<E> h, Node<E> p) {
	// p 被设置为新的 head
        if (h != p && casHead(h, p))
            // h 的next 指向自己,成为哨兵
            h.lazySetNext(h);
}
void lazySetNext(Node<E> val) {
 	    // 存储变量的引用到对象的指定的偏移量处
 	    // 有延迟,非立即可见
            UNSAFE.putOrderedObject(this, nextOffset, val);
}

总结

  1. 同尾插一样,头删时,head的更新也是有拖延现象的,每两次删除更新一下。
  2. 核心思想同样是for循环,采用CAS进行更新。
  3. 如果head的item为null(比如刚开始时,或者未更新),进入最后一个分支,p == q(p.next)直接指向下一个节点,即队列的第一个元素。
  4. 如果如果 p 不为 null,进入第一个分支,通过 CAS 来循环设置 p 指向的元素为null,成功则返回删除的元素值。如果这是第二次大循环,即从步骤3过来的,会有p != h,直接CAS更新head
  5. 如果队列的第一个节点(p.next)的元素为空,则已被其它线程修改,此时进入第二个分支,直接更新头结点。
  6. 遇到哨兵节点,进入第三个分支,直接从新的 head 开始查找。
  7. 对于updateHead方法,原有的head成为了哨兵,此时head和tail就就指向同一个元素,不论是offer()方法,还是poll()方法,都有可能访问到这个哨兵节点。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值