034_java.util.concurrent.ConcurrentLinkedQueue

继承体系

image.png
要实现一个线程安全的队列有两种方式:阻塞和非阻塞。阻塞队列无非就是锁的应用,而非阻塞则是CAS算法的应用。ConcurrentLinkedQueue是一个基于链接节点的无边界的线程安全队列,它采用FIFO原则对元素进行排序。采用“wait-free”算法(即CAS算法)来实现的。

重要字段

private transient volatile Node<E> head;
private transient volatile Node<E> tail;

private static class Node<E> {
    /** 节点元素域 */
    volatile E item;
    volatile Node<E> next;

    //初始化,获得item 和 next 的偏移量,为后期的CAS做准备

    Node(E item) {
        UNSAFE.putObject(this, itemOffset, item);
    }

    boolean casItem(E cmp, E val) {
        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) {
        return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
    }

    // Unsafe mechanics

    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);
        }
    }
}

CoucurrentLinkedQueue的结构由head节点和tail节点组成,每个节点由节点元素item和指向下一个节点的next引用组成,而节点与节点之间的关系就是通过该next关联起来的,从而组成一张链表的队列。节点Node为ConcurrentLinkedQueue的内部类。

构造函数

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

ConcurrentLinkedQueue的构造方法也简单,就是将首尾结点进行赋值,挂一个空结点上去。

重要方法

入队方法

入列,我们认为是一个非常简单的过程:tail节点的next执行新节点,然后更新tail为新节点即可。从单线程角度我们这么理解应该是没有问题的,但是多线程呢?如果一个线程正在进行插入动作,那么它必须先获取尾节点,然后设置尾节点的下一个节点为当前节点,但是如果已经有一个线程刚刚好完成了插入,那么尾节点是不是发生了变化?对于这种情况ConcurrentLinkedQueue怎么处理呢?我们先看源码:

offer(E e):将指定元素插入都队列尾部:

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()方法中介绍。

出队方法

ConcurrentLinkedQueue提供了poll()方法进行出列操作。入列主要是涉及到tail,出列则涉及到head。我们先看源码:

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()方法,都有可能访问到这个哨兵节点。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值