• 并发编程(十一):非阻塞队列ConcurrentLinkedQueue



    1.线程安全队列

    两种方式:

    • 阻塞算法:出队入队用同一把锁或分别用一把锁实现
    • 非阻塞算法:循环CAS的方式实现

    ConcurrentLinkedQueue是基于链接节点的无界线程安全队列,添加元素到队尾,从头部获取元素


    2.ConcurrentLinkedQueue结构

    每个节点由节点元素item和指向下一个节点的引用next构成
    默认情况下head节点存储的元素为空,head节点=tail节点

    private transient volatile Node<E> tail=head;
    

    3.入队列

    3.1 入队列过程

    将节点添加到尾部,示意图:

    区分tail节点和尾结点:尾结点可能是tail节点,也可能是tail.next节点

    插入元素时,tail.next不为空时,则将tail节点移到新节点,加入尾节点后面(tail每次移动两个节点)

    入队源代码如下:

    public boolean offer(E e) {
        if (e == null) throw new NullPointerException()
        // 入队前,创建一个入队节点 
        Node<E> n = new Node<E>(e);
        retry:
        // 死循环,入队不成功反复入队。 
        for (;;) {
        	// 创建一个指向tail节点的引用 (非尾结点)
            Node<E> t = tail;
            // p用来表示队列的尾节点,默认情况下等于tail节点。 
            Node<E> p = t;
            for (int hops = 0; ; hops++) {
            	// 获得p节点的下一个节点。 
                Node<E> next = succ(p);
    			// next节点不为空,说明p不是尾节点,需要更新p后在将它指向next节点 
                if (next != null) {
                	// 循环了两次及其以上,并且当前节点还是不等于尾节点,HOPS默认值1
                    if (hops > HOPS && t != tail)
                        continue retry;
                    p = next;
                }
                // 如果p是尾节点,则设置p节点的next节点为入队节点。 
                else if (p.casNext(null, n)) { 
    			/*如果tail节点有大于等于1个next节点,则将入队节点设置成tail节点, 
    更新失败了也没关系,因为失败了表示有其他线程成功更新了tail节点*/
                    if (hops >= HOPS)
                        // CAS更新tail节点,允许失败 
                        casTail(t, n); 
                    return true;
                }
    		   //p有next节点,表示p的next节点是尾节点,则重新设置p节点 
                else {
                    //p后移操作
                    p = succ(p);
                }
            }
        }
    }
    

    主要做两件事情:

    • 定位尾结点
    • 使用cas算法将尾结点的下一个节点(p.next)设置为入队节点,不成功则重试

    3.2 定位尾结点

    通过tail节点找到尾结点,尾结点可能是tail节点,也可能是tail的尾结点

    succ(p)代码如下:

    final Node<E> succ(Node<E> p) { 
        //获取下一个节点
    	Node<E> next = p.getNext(); 
        //p == next只有初始化时才会出现,p后移
    	return (p == next) head : next; 
    } 
    

    3.3 设置入队节点为尾结点

    p.casNext(null,n)将入队节点设置为当前队列尾结点的next节点(现在的p节点)

    • p为null,表示是尾节点,更新该节点值
    • p不为null,表示有其他线程更新,重新获取尾结点

    3.4 HOPS的设计意图

    tail节点和尾结点的距离大于HOPS(默认1)时才会更新tail节点,因为tail是volatile变量,写操作的开销比读操作的开销大,本质上是通过增加volatile读的次数减少volatile写的次数,达到性能提升的效果


    4.出队列

    示意图:

    只有当head节点没有元素并且出队时,才会更新head节点

    也是通过HOPS来减少CAS对head节点的更新,提高出队效率

    代码如下:

    public E poll() {
        Node<E> h = head;
    	// p表示头节点,需要出队的节点Node<E> p = h; 
        for (int hops = 0;; hops++) {
            // 获取p节点的元素 
            E item = p.getItem();
            // 如果p节点的元素不为空,使用CAS设置p节点引用的元素为null, 
            // 如果成功则返回p节点的元素。 
            if (item != null && p.casItem(item, null)) {
                if (hops >= HOPS) {
                	// 将p节点下一个节点设置成head节点 
                    Node<E> q = p.getNext();
                    updateHead(h, (q != null) q : p);
                }
                return item;
            }
    		// 如果头节点的元素为空或头节点发生了变化,这说明头节点已经被另外 
    		// 一个线程修改了。那么获取p节点的下一个节点 
            Node<E> next = succ(p);
    		// 如果p的下一个节点也为空,说明这个队列已经空了 
            if (next == null) {
    		   // 更新头节点。 
                updateHead(h, p);
                break;
            }
    		// 如果下一个元素不为空,则将头节点的下一个节点设置成头节点 
            p = next;
        }
        return null;
    }
    

  • 相关阅读:
    菜鸟版JAVA设计模式—从买房子看代理模式
    NTP工作机制及时间同步的方法
    Java工厂模式
    圣魔大战3(Castle Fantisia)艾伦希亚战记完美攻略
    对javabean的内省操作
    插入排序(insertion sort)
    中英文对照 —— 十二星座
    中英文对照 —— 十二星座
    数学归纳法的相关证明
    数学归纳法的相关证明
  • 原文地址:https://www.cnblogs.com/kenshine/p/14520557.html
Copyright © 2020-2023  润新知