并发编程(十一):非阻塞队列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;
}

posted @ 2021-03-11 21:10  菜鸟kenshine  阅读(83)  评论(0编辑  收藏  举报