在并发编程中,有时候需要使用线程安全的队列。如果要实现一个线程安全的队列有两种方式:
①阻塞算法:加锁
②非阻塞算法:CAS
于是乎,Doug Lea大神编写了ConcurrentLinkedQueue,一个基于链表的线程安全队列。它采用先进先出的规则对节点进行排序,当我们添加一个元素时,它会添加到尾部;当我们删除一个元素时,它会返回队列的头部元素。
一、ConcurrentLinkedQueue的结构(JDK1.8)
ConcurrentLinkedQueue主要由head节点和tail节点组成,head和tail都是一个静态内部类Node,每个Node由item和下一个节点next组成,默认情况head和tail相等。
public ConcurrentLinkedQueue() {
head = tail = new Node<E>(null);
}
二、ConcurrentLinkedQueue的方法剖析
①offer(E e)
offer方法用来入队(添加元素),入队列就是将入队节点添加到队列的尾部。下面是offer方法的源代码:
public boolean offer(E e) {
//检查是否为null 此处证明ConcurrentLinkdQueue不可以存放null值
checkNotNull(e);
//将新元素封装为Node
final Node<E> newNode = new Node<E>(e);
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
if (q == null) {
//如果p是tail节点,把新元素赋值到p.next,采用CAS
if (p.casNext(null, newNode)) {
//若为首次添加元素,不会设置tail,设置tail存在滞后性
//从而减少CAS设置tail的时间
if (p != t) // hop two nodes at a time
casTail(t, newNode); // Failure is OK.
return true;
}
// Lost CAS race to another thread; re-read next
}
//由于tail的滞后性,可能出队时会删除tail节点,
//因此给tail复位
else if (p == q)
// We have fallen off list. If tail is unchanged, it
// will also be off-list, in which case we need to
// jump to head, from which all live nodes are always
// reachable. Else the new tail is a better bet.
p = (t != (t = tail)) ? t : head;
else
// Check for tail updates after two hops.
//移动tail
p = (p != t && t != (t = tail)) ? t : q;
}
}
下面我们通过一个例子来说明ConcurrentLinkedQueue的offer方法,向一个ConcurrentLinkedQueue添加张三、李四和王五三个元素。
在jdk1.6时,ConcurrentLinkedQueue采用的是HOPS设计,采用一个hops变量来控制并减少tail节点的更新频率,其实就是tail设置的滞后性,这个常量默认为1,和现在p != t的设计相同。(ps:这里就得说一下方腾飞作者的《Java并发编程的艺术》,都2022年了,jdk都17出啦,不更新一波第二版??虽然这本书还是非常值得看)
②poll()
出队列就是返回头节点的元素。下面是poll方法的源代码:
public E poll() {
//跳转的tag
restartFromHead:
//CAS循环
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
//CAS将item置空
if (item != null && p.casItem(item, null)) {
// Successful CAS is the linearization point
// for item to be removed from this queue.
if (p != h) // hop two nodes at a time
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
final void updateHead(Node<E> h, Node<E> p) {
if (h != p && casHead(h, p))
//懒加载自己指向自己 等待JVM回收
h.lazySetNext(h);
}
我们还是通过上边添加元素的例子,逐个删除张三、李四、王五。
至此全部删除完成,删除的大致操作就是,获取头节点的item,看item是不是为空,为空则向下移动,移动head节点也是有一个节点的滞后性。
但是,此时tail指向一个null节点,会被jvm所回收,如果我们再添加一个节点呢?假设我们再添加一个节点"赵六"。
三、总结
ConcurrentLinkedQueue的介绍就到此结束啦,在此做一个总结:
①ConcurrentLinkedQueue不可以插入null值
②ConcurrentLinkedQueue并不是每次入队都更新tail节点,也不是每次出队都更新head节点,具有一个节点的滞后性,JDK1.6之前使用了一个HOPS的变量控制滞后性,1.7以后使用的是节点是否相等控制滞后性。
③ConcurrentLinkedQueue是非阻塞的,采用了CAS来控制并发实现线程安全。
今天的分享就到此结束啦,喜欢的小伙伴记得点赞呦。
参考资料:《Java并发编程的艺术》
关注公众号JavaGrowUp,下期不迷路,获取更多精彩内容。