文章目录
一、:ConcurrentLinkedQueue简介
队列是我们常用的一种数据结构,为了解决线程安全的问题,Doug Lea大师为我们准备了ConcurrentLinkedQueue这个线程安全的队列。从类名就可以看出实现队列的数据结构是链。
要实现一个线程安全的队列有两种方式:阻塞和非阻塞。阻塞无非就是锁的应用,而非阻塞则是CAS算法的应用。
ConcurrentLinkedQueue是一个基于链的无边界的线程安全队列,它采用FIFO原则对元素进行排序。采用“wait-free”算法(即CAS算法)来实现的。
CoucurrentLinkedQueue规定了如下几个不变性:
●入队的最后一个元素的next为null。
●队列中所有未删除的节点的item都不能为null且都能从head节点遍历到。
●对于要删除的节点,不是直接将其设置为null,而是先将其item域设置为null(迭代器会跳过item为null的节点)。
●允许head和tail更新滞后。就是说head、tail不总是指向第一个元素和最后一个元素。
head的不变性和可变性:
不变性:
●所有未删除的节点都可以通过head节点遍历到。
●head不能为null。
●head节点的next不能指向自身。
可变性:
●head的item可能为null,也可能不为null。
●允许tail滞后head,也就是说调用succ()方法,从head不可达tail。
tail的不变性和可变性:
不变性:
●tail不能为null。
可变性:
●tail的item可能为null,也可能不为null。
●tail节点的next域可以指向自身。
●允许tail滞后head,也就是说调用succ()方法,从head不可达tail。
二、:关键属性及类
①关键属性
private transient volatile Node<E> head;
private transient volatile Node<E> tail;
②关键类
private static class Node<E> {
volatile E item;
volatile Node<E> next;
}
三、:重点方法分析
①构造方法
public ConcurrentLinkedQueue() {
// 初始化头尾节点
head = tail = new Node<E>(null);
}
public ConcurrentLinkedQueue(Collection<? extends E> c) {
Node<E> h = null, t = null;
// 遍历c,并把它元素全部添加到单链表中
for (E e : c) {
checkNotNull(e);
Node<E> newNode = new Node<E>(e);
if (h == null)
h = t = newNode;
else {
t.lazySetNext(newNode);
t = newNode;
}
}
if (h == null)
h = t = new Node<E>(null);
head = h;
tail = t;
}
从两个构造方法可以看出,ConcurrentLinkedQueue是一个无界的单链表实现的队列。
②add()方法
public boolean add(E e) {
return offer(e);
}
public boolean offer(E e) {
// 检查节点是否为null
checkNotNull(e);
// 创建新节点
final Node<E> newNode = new Node<E>(e);
// 自旋
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
// q == null 表示p已经是最后一个节点了,尝试入队
if (q == null) {
// p is last node
// CAS更新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".
// p != t 说明有其它线程抢先一步更新tail,把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
}
// p == q 代表着该节点已经被删除了(出队的逻辑)
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的值
p = (t != (t = tail)) ? t : head;
// tail并没有指向真正的尾节点
else
// Check for tail updates after two hops.
// 将p指向真正的尾节点
p = (p != t && t != (t = tail)) ? t : q;
}
}
代码主要流程如下:
●定位到队尾,尝试把新节点放到后面入队。
●如果尾部变化了,则重新获取尾部,再次重试。
这里注意一个问题,tail是延迟更新的,每次不一定指向的是真正的尾节点,因此出现了p,每次会去定位真正的尾节点。同时,CAS设置尾节点的操作也就可以不需要判断成功与否,因为如果失败了,会在后续循环继续更新尾节点。
此外,在寻找尾节点的判断条件中,包含了对高并发的处理逻辑,可以细细品味其中的奥妙。
③poll()方法
public E poll() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
// 如果p节点的元素不为null,则通过CAS来设置p节点的元素为null,如果成功则返回p节点的元素
if (item != null && p.casItem(item, null)) {
// Successful CAS is the linearization point
// for item to be removed from this queue.
// p != h 代表着节点未被删除,更新head
if (p != h) // hop two nodes at a time
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
// 如果头节点的元素为空或头节点发生了变化,说明头节点已经被另外的线程修改了
// 获取p节点的下一个节点,如果p节点的下一节点为null,说明队列已经空了
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
// p == q 代表着该节点已经被删除了,使用新的head重新开始
else if (p == q)
continue restartFromHead;
// 如果下一个元素不为空,则将头节点的下一个节点设置成头节点
else
p = q;
}
}
}
final void updateHead(Node<E> h, Node<E> p) {
if (h != p && casHead(h, p))
// 制造哨兵节点
h.lazySetNext(h);
}
代码主要流程如下:
●定位到头节点,尝试更新其值为null,如果成功了,就出队。
●如果更新失败或者头节点变化了,就重新寻找头节点,并重试。
●针对出队的元素,结点的next域指向自己。
整个出队过程没有一点阻塞相关的代码,所以出队的时候不会阻塞线程,没找到元素就返回null。
同理,head是延迟更新的,每次不一定指向的是真正的头节点。
这里制造了哨兵节点(节点的next也指向它自己)。通常这种节点在队列中存在的价值不大,一般表示为要删除的节点或者是空节点。
④peek()方法
public E peek() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
if (item != null || (q = p.next) == null) {
updateHead(h, p);
return item;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
此方法也会返回头节点,但是不会执行出队操作。
⑤remove()方法
public boolean remove(Object o) {
if (o != null) {
Node<E> next, pred = null;
for (Node<E> p = first(); p != null; pred = p, p = next) {
boolean removed = false;
E item = p.item;
// 节点元素不为null
if (item != null) {
// 若不匹配,则获取next节点继续匹配
if (!o.equals(item)) {
next = succ(p);
continue;
}
// 若匹配,则通过CAS操作将对应节点元素置为null
removed = p.casItem(item, null);
}
// 获取删除节点的后继节点
next = succ(p);
// 将被删除的节点移出队列
if (pred != null && next != null) // unlink
pred.casNext(p, next);
if (removed)
return true;
}
}
return false;
}
四、:总结
①特征
●ConcurrentLinkedQueue不是阻塞队列。
●ConcurrentLinkedQueue不能用在线程池中(无界)。
●ConcurrentLinkedQueue使用(CAS+自旋)更新头尾节点控制出队入队操作。
②非阻塞算法的实现
●使用CAS指令处理并发访问是基本操作。
●head/tail并非总是指向队列的头/尾节点,也就是说允许队列处于不一致状态。 这个特性把入/出队时,原本需要一起原子化执行的两个步骤分离开来,从而缩小了入/出队时需要原子化更新值的范围到唯一变量。这是非阻塞算法得以实现的关键。
●由于队列有时会处于不一致状态。为此,ConcurrentLinkedQueue使用三个不变式来维护非阻塞算法的正确性。
●以批处理方式来更新head和tail,从整体上减少入/出队操作的开销。
●为了有利于垃圾收集,队列使用特有的head更新机制;为了确保从已删除节点向后遍历,可到达所有的非删除节点,队列使用了特有的向后推进策略。
③HOPS的设计
通过上面对offer和poll方法的分析,我们发现tail和head是延迟更新的,两者更新触发时机为:
●tail更新触发时机:当tail指向的节点的下一个节点不为null的时候,会执行定位队列真正的队尾节点的操作,找到队尾节点后完成插入之后才会通过casTail进行tail更新;当tail指向的节点的下一个节点为null的时候,只插入节点不更新tail。
●head更新触发时机:当head指向的节点的item域为null的时候,会执行定位队列真正的队头节点的操作,找到队头节点后完成删除之后才会通过updateHead进行head更新;当head指向的节点的item域不为null的时候,只删除节点不更新head。
在offer()源码中有注释:hop two nodes at a time。所以这种延迟更新的策略就被叫做HOPS?通过分析源码可以看出,head和tail的更新是“跳着的”,即中间总是间隔了一个。那么这样设计的意图是什么呢?
如果让tail永远作为队列的队尾节点,实现的代码量会更少,而且逻辑更易懂。但是,这样做有一个缺点,如果大量的入队操作,每次都要执行CAS进行tail的更新,汇总起来对性能也会是大大的损耗。如果能减少CAS更新的操作,无疑可以大大提升入队的操作效率,所以代码每间隔1次(tail和队尾节点的距离为1)进行才利用CAS更新tail,对head的更新也是同样的道理。虽然这样设计会多出在循环中定位队尾节点,但总体来说读的操作效率要远远高于写的性能,因为多出来的在循环中定位尾节点的操作的性能损耗相对而言是很小的。
系列文章传送门:
JUC探险-1、初识概貌
JUC探险-2、synchronized
JUC探险-3、volatile
JUC探险-4、final
JUC探险-5、原子类
JUC探险-6、Lock & AQS
JUC探险-7、ReentrantLock
JUC探险-8、ReentrantReadWriteLock
JUC探险-9、Condition
JUC探险-10、常见工具、数据结构
JUC探险-11、ConcurrentHashMap
JUC探险-12、CopyOnWriteArrayList
JUC探险-13、ConcurrentLinkedQueue
JUC探险-14、ConcurrentSkipListMap
JUC探险-15、BlockingQueue
JUC探险-16、ThreadLocal
JUC探险-17、线程池