简述
当需要实现一个线程安全的队列有两 种方式:一种是使用阻塞算法,另一种是使用非阻塞算法。使用阻塞算法的队列可以用一个锁 (入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现。非阻塞的实现方式则可以使用循环CAS的方式来实现。
ConcurrentLinkedQueue是无界线程安全队列(FIFO),基于CAS来实现。
ConcurrentLinkedQueue
ConcurrentLinkedQueue由head节点和tail节点来管理队列
属性
ConcurrentLinkedQueue
private transient volatile Node head;
private transient volatile Node tail;
复制代码
重要内部类
Node节点有两个属性,item存储节点元素,next指针指向下一个节点的引用,从而组成链表结构的队列。两个属性都用volatile修饰,为了保证内存可见性。
private static class Node {
volatile E item;
volatile Node next;
/**
* Constructs a new node. Uses relaxed write because item can
* only be seen after publication via casNext.
*/
Node(E item) {
UNSAFE.putObject(this, itemOffset, item);
}
...
}
复制代码
构造方法
/**
* 默认无参构造
* tail节点等于head节点,
*/
public ConcurrentLinkedQueue() {
head = tail = new Node(null);
}
/**
* 指定集合,以集合的迭代器的遍历顺序添加
*/
public ConcurrentLinkedQueue(Collection c) {
Node h = null, t = null;
// foreach遍历
for (E e : c) {
//校验节点是否为空
checkNotNull(e);
Node newNode = new Node(e);
if (h == null)
h = t = newNode;
else {
t.lazySetNext(newNode);
t = newNode;
}
}
if (h == null)
h = t = new Node(null);
head = h;
tail = t;
}
复制代码
offer方法
public boolean offer(E e) {
// 判空
checkNotNull(e);
final Node newNode = new Node(e);
for (Node t = tail, p = t;;) {
Node q = p.next;
// 若p是尾节点
if (q == null) {
// CAS将新节点newNode置为当前队列尾节点p的next节点
if (p.casNext(null, newNode)) {
// Successful CAS is the linearization point
// for e to become an element of this queue,
// and for newNode to become "live".
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
}
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
// p有next节点,表示p的next节点是尾节点,则需要重新更新p后将它指向next节点
p = (p != t && t != (t = tail)) ? t : q;
}
}
复制代码
根据源码单线程角度来理解此方法:
默认构造函数:
添加节点A:
走if分支,CAS将节点A置为当前队列尾节点p的next节点(若cas失败再次重试),由于p=t所以没执行casTail方法
添加节点B:
第一次循环:t、p节点:node节点,q节点:A节点(图中),走else分支
p = (p != t && t != (t = tail)) ? t : q;
复制代码
由于p=t,所以p赋为q(图中A节点)
第二次循环:t节点:node节点,p:A节点,q为null,进入第一个分支,cas将p的next指向新节点,p不等于t,通过casTail()方法,将新节点设置为队列的队尾节点(若casTail()失败等待下次添加)
offer主要做两件事:
1.将入队节点设置成当前队列尾节点的下一个节点
2.更新tail节点,如果tail节点的next节 点不为空,则将入队节点设置成tail节点,如果tail节点的next节点为空,则将入队节点设置成 tail的next节点,所以tail节点不总是尾节点
多线程角度理解:
如果有一个线程正在 入队,那么它先获取到尾节点,然后设置尾节点的下一个节点为入队节点,但这时可能有另 外一个线程插队了,那么队列的尾节点就会发生变化,这时当前线程要暂停入队操作,然后重 新获取尾节点。
当线程A获取到tail节点。这时线程B插队先行完成offer操作,修改了tail节点,再执行
t != (t = tail),线程A前后两次读取的变量t指向的节点明显不相同,那么p将会重新指向尾节点
类似于这个demo:
public class Test {
private Integer age;
public void setAge(Integer age) {
this.age = age;
}
public Integer getAge() {
return age;
}
public static void main(String[] args) throws InterruptedException {
final Test test = new Test();
Integer age = test.getAge();
new Thread(new Runnable() {
@Override
public void run() {
test.setAge(100);
}
}).start();
Thread.sleep(1000);
System.out.println(age != (age = test.getAge()) ? test.getAge() : 0); //100
}
}
复制代码
poll方法
public E poll() {
//设置起始点
restartFromHead:
for (;;) {
for (Node h = head, p = h, q;;) {
// 获取p节点的值
E item = p.item;
// 如果p节点的值不为空,CAS将p节点的值置为null
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
// //如果p节点不是head节点则更新head节点,也可以理解为删除该结点后检查head是否与头结点相差两个结点,如果是则更新head节点
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
//如果p节点的下一个节点为null,则说明这个队列为空,更新head结点
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
//结点出队失败,重新跳到restartFromHead来进行出队
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
final void updateHead(Node h, Node p) {
// cas更新head节点
if (h != p && casHead(h, p))
// //将旧的头结点指向自身以实现删除
h.lazySetNext(h);
}
复制代码
当前ConcurrentLinkedQueue:
第一次调用:由于head的item为空,走elas分支,p指向p的next节点(图中节点A),节点A的item不为null,cas将其item置为null(若cas失败再次重试),h仍然指向head节点,所以p != h,cas更新head节点:
第二次调用:直接进入if分支,但因为p=h,所以不更新head节点,head节点值为null
当head节点里有元素时,直接弹出head节点里的元素,而不会更新head节点。
只有当head节点里没有元素时,出队操作才会更新head节点,head节点不一定是头节点
HOPS
HOPS即跃数,通过解析offer以及poll方法可以看到tail、head节点并不一定是头、尾结点。
tail更新触发时机:当tail指向的节点的下一个节点不为null的时候,会执行定位队列真正的队尾节点的操作,找到队尾节点后完成插入之后才会通过casTail进行tail更新;当tail指向的节点的下一个节点为null的时候,只插入节点不更新tail
head更新触发时机:当head指向的节点的item域为null的时候,会执行定位队列真正的队头节点的操作,找到队头节点后完成删除之后才会通过updateHead进行head更新;当head指向的节点的item域不为null的时候,只删除节点不更新head。
那么为什么这样设计?
我们以下方式实现offer方法:
public boolean offer(E e) {
if (e == null) {
throw new NullPointerException();
}
Node n = new Node(e);
for (; ; ) {
Node t = tail;
if (t.casNext(null, n) && casTail(t, n)) {
return true;
}
}
}
复制代码
让tail节点永远作为队列的尾节点,这样实现代码量非常少,而且逻辑清晰和易懂。但这样做有一个缺点,如果同时有大量的入队操作,每次都要CAS更新tail,汇总起来对性能也会是大大的损耗。如果能减少CAS更新tail节点的次数,就能提高入队的效率,所以才控制并减少tail节点的更新频率。但是又不能把HOPS值(默认等于1)设太大,因为太大的话就会需要多次循环才能定位出尾结点,但总体来说读的操作效率要远远高于写的性能。
感谢
《java并发编程的艺术》
https://juejin.im/post/5aeeae756fb9a07ab11112af#heading-6