Java并发编程之ConcurrentLinkedQueue
引言: 我们要实现一个线程安全的队列有两种实现方式一种是使用阻塞算法,另一种是使用非阻塞算法。使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现,而非阻塞的实现方式则可以使用循环CAS的方式来实现。
简介: ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。它采用了“wait-free”算法来实现,该算法在Michael & Scott算法上进行了一些修改。
ConcurrentLinkedQueue主要方法详解:
1、构造函数:我们可以清楚的看到ConcurrentLinkedQueue内部的队列是使用单向链表方式实现,类中两个volatile 类型的Node 节点分别用来存放队列的首位节点。通过无参构造函数可知默认头尾节点都是指向 item 为 null 的哨兵节点。Node节点内部则维护一个volatile 修饰的变量item 用来存放节点的值,next用来存放链表的下一个节点,从而链接为一个单向无界链表,这就是单向无界的根本原因。
// 默认构造方法,head节点存储的元素为空,tail节点等于head节点
public ConcurrentLinkedQueue() {
head = tail = new Node<E>(null);
}
// 根据其他集合来创建队列
public ConcurrentLinkedQueue(Collection<? extends E> c) {
Node<E> h = null, t = null;
// 遍历节点
for (E e : c) {
// 若节点为null,则直接抛出NullPointerException异常
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;
}
2、offer()入队方法:入队的实质就是在队尾做节点插入,具体的执行流程如下:
1.调用checkNotNull方法判断待入队元素是否为null,如果为nu抛出空指针异常;
2.创建一个待入队节点;
3.循环执行队尾插入:
情况1: 如果tail节点的下一个节点q为nul,通过p.casNext(nullnewNode)将p节点的next节点设置为待入队节点:
· CAS设置成功:比较p和t,如果p不等t,将tail节点设置为待入队节点,入队成功,返回true,如果p等于t,直接返回true;
· CAS设置不成功,表明有其他的线程对tail节点有所更改,那么,继续执行for循环,直到入队成功。
情况2: 如果p和t相等,则将p指向q,否则,判断tail节点是否发生变化,如果没有发生变化,将p指向q,如果发生变化,设置p为尾节点;
情况3:通过情况2的操作,p和q相等的情况就可能会出现了,此时,若tail节点没有发生变化,那么应该就是head节点发生了变化,设置p为head节点,从头开始遍历队列,如果是tail节点发生变化,设置p为tail节点。
public boolean add(E e) {
return offer(e);
}
public boolean offer(E e) {
// 如果e为null,则直接抛出NullPointerException异常
checkNotNull(e);
// 创建入队节点
final Node<E> newNode = new Node<E>(e);
// 循环CAS直到入队成功
// 1、根据tail节点定位出尾节点(last node);2、将新节点置为尾节点的下一个节点;3、casTail更新尾节点
for (Node<E> t = tail, p = t;;) {
// p用来表示队列的尾节点,初始情况下等于tail节点
// q是p的next节点
Node<E> q = p.next;
// 判断p是不是尾节点,tail节点不一定是尾节点,判断是不是尾节点的依据是该节点的next是不是null
// 如果p是尾节点
if (q == null) {
// p is last node
// 设置p节点的下一个节点为新节点,设置成功则casNext返回true;否则返回false,说明有其他线程更新过尾节点
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
}
// 多线程操作时候,由于poll时候会把旧的head变为自引用,然后将head的next设置为新的head
// 所以这里需要重新找新的head,因为新的head后面的节点才是激活的节点
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.
p = (p != t && t != (t = tail)) ? t : q;
}
}
public boolean offer(E e) {
checkNotNull(e);
final Node<E> newNode = new Node<E>(e);
for (;;) {
Node<E> t = tail;
if (t.casNext(null ,newNode) && casTail(t, newNode)) {
return true;
}
}
}
3、poll()出队操作:出队的实质就是清空表头节点的引用并返回表头节点的值,具体的逻辑如下:
1.获取头结点的元素;
2.如果表头节点的元素不为null,并且调用p.casltem(item,nul)设置表头节点数据为null成功:
·如果p不等于head节点,此时表头发生了变化,调用updateHead方法更新表头节点,然后返回删除节点item;
·否则,不更新表头节点,直接返回删除节点item。
3.如果步骤2条件不成立并且表头节点的next节点q为null,那么此时队列只有一个为nul的节点,调用updateHead方法更新表头节点为p,然后返回nul;
4.如果步骤2和3的条件均不成立并且p等于q,跳转到restartFromHead标记重新执行;
5.步骤2,3,4均不成立,将p指向q;
6.循环执行上述步骤;
public E poll() {
restartFromHead:
for (;;) {
// p节点表示首节点,即需要出队的节点
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;
}
}
}
与LinkedBlockingQueue 对比及二者的适用场景:
使用阻塞队列的好处:多线程操作共同的队列时不需要额外的同步,另外就是队列会自动平衡负载,即哪边(生产与消费两边)处理快了就会被阻塞掉,从而减少两边的处理速度差距。ConcurrentLinkedQueue 适用于高并发读写操作,理论上有最高的吞吐量,无界,不保证数据访问实时一致性,Iterator不抛出并发修改异常,采用CAS机制实现无锁访问。
LinkedBlockingQueue 多用于任务队列。
ConcurrentLinkedQueue 多用于消息队列。
对于多个生产者,对于LBQ性能还算可以接受,但是多个消费者就不行了mainLoop需要一个timeout的机制,否则空转,cpu会飙升的。LBQ正好提供了timeout的接口,更方便使用。如果CLQ,那么我需要收到处理sleep
单生产者,单消费者:用 LinkedBlockingqueue
多生产者,单消费者:用 LinkedBlockingqueue
单生产者 ,多消费者 :用 ConcurrentLinkedQueue
多生产者 ,多消费者:用 ConcurrentLinkedQueue
本文参考
本文主要参考以下文章,谨以技术分享为目的,将此文搬到CSDN上,如有侵权问题请联系本人,乐于分享提高。
作者: miaoLoveCode
链接:https://www.jianshu.com/p/24516e7853d1