ConcurrentLinkedQueue详解

ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列, 它采用先进先出的规则对节点进行排序, 当我们添加一个元素的时候, 它会添加到队列的尾部; 当我们获取一个元素时, 它会返回队列头部的元素。 它采用了“wait-free”算法(即CAS算法) 来实现, 该算法在Michael&Scott算法上进行了一些修改。

一、ConcurrentLinkedQueue的结构

ConcurrentLinkedQueue由 head节点和tail节点组成, 每个节点(Node) 由节点元素(item) 和指向下一个节点(next) 的引用组成, 节点与节点之间就是通过这个next关联起来, 从而组成一张链表结构的队列。 默认情况下head节点存储的元素为空, tail节点等于head节点。

private transient volatile Node<E> tail = head;

二、入队列

1、入队列就是将入队节点添加到队列的尾部。

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) {
		// 循环了两次及其以上, 并且当前节点还是不等于尾节点
		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)
		casTail(t, n) ; // 更新tail节点, 允许失败
		return true;
		}// p有next节点, 表示p的next节点是尾节点, 则重新设置p节点
		else {
		p = succ(p) ;
		}
		}
		}
	}
从源代码角度来看, 整个入队过程主要做两件事情: 第一是定位出尾节点; 第二是使用CAS算法将入队节点设置成尾节点的next节点, 如不成功则重试。
如果有一个线程正在入队, 那么它必须先获取尾节点, 然后设置尾节点的下一个节点为入队节点, 但这时可能有另外一个线程插队了, 那么队列的尾节点就会发生变化, 这时当前线程要暂停入队操作, 然后重新获取尾节点。
2、定位尾节点
tail节点并不总是尾节点, 所以每次入队都必须先通过tail节点来找到尾节点。 尾节点可能是tail节点, 也可能是tail节点的next节点。 代码中循环体中的第一个if就是判断tail是否有next节点, 有则表示next节点可能是尾节点。 获取tail节点的next节点需要注意的是p节点等于p的next节点的情况, 只有一种可能就是p节点和p的next节点都等于空, 表示这个队列刚初始化, 正准
备添加节点, 所以需要返回head节点。 获取p节点的next节点代码如下。
final Node<E> succ(Node<E> p) {
	Node<E> next = p. getNext() ;
	return (p == next) head : next;
}
3、设置入队节点为尾节点
p.casNext(null, n) 方法用于将入队节点设置为当前队列尾节点的next节点, 如果p是null,表示p是当前队列的尾节点, 如果不为null, 表示有其他线程更新了尾节点, 则需要重新获取当前队列的尾节点。
4、HOPS的设计意图 
private static final int HOPS = 1;
让tail节点永远作为队列的尾节点, 这样实现代码量少, 逻辑清晰易懂。 但缺点是, 每次都需要使用循环CAS更新tail节点。 如果能减少CAS更新tail节点的次数, 就能提高入队的效率, 所以可以使用hops变量来控制并减少tail节点的更新频率, 并不是每次节点入队后都将tail节点更新成尾节点, 而是当 tail节点和尾节点的距离大于等于常量HOPS的值(默认等于1) 时才更新tail节点, tail和尾节点的距离越长, 使用CAS更新tail节点的次数就会越少, 但是距离越长带来的负面效果就是每次入队时定位尾节点的时间就越长, 因为循环体需要多循环一次来定位出尾节点, 但是这样仍然能提高入队的效率, 因为从本质上来看它通过增加对volatile变量的读操作来减少对volatile变量的写操作, 而对volatile变量的写操作开销要远远大于读操作, 所以入队效率会有所提升。
三、出队列
出队列的就是从队列里返回一个节点元素, 并清空该节点对元素的引用。
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) ;
		}r
		eturn item;
		}/
		/ 如果头节点的元素为空或头节点发生了变化, 这说明头节点已经被另外
		// 一个线程修改了。 那么获取p节点的下一个节点
		Node<E> next = succ(p) ;
		// 如果p的下一个节点也为空, 说明这个队列已经空了
		if (next == null) {
		// 更新头节点。
		updateHead(h, p) ;
		break;
		}/
		/ 如果下一个元素不为空, 则将头节点的下一个节点设置成头节点
		p = next;
		}r
		eturn null;
}
首先获取头节点的元素, 然后判断头节点元素是否为空, 如果为空, 表示另外一个线程已经进行了一次出队操作将该节点的元素取走, 如果不为空, 则使用CAS的方式将头节点的引用设置成null, 如果CAS成功, 则直接返回头节点的元素, 如果不成功, 表示另外一个线程已经进行了一次出队操作更新了head节点, 导致元素发生了变化, 需要重新获取头节点。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值