在并发编程中有时候需要使用线程安全的队列。要实现一个线程安全的队列有两种实现方式:一种是使用阻塞算法,另一种是使用非阻塞算法。使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现,而非阻塞的实现方式则可以使用循环CAS的方式来实现。ConcurrentLinkedQueue是使用非阻塞的方式来实现线程安全队列的。
1.ConcurrentLinkedQueue
ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,采用先进先出的规则对节点进行排序。添加一个元素的时候,它会添加到队列的尾部,获取一个元素时,它会返回队列头部的元素。
ConcurrentLinkedQueue的节点都是Node类型的,源码如下:
private static class Node<E> {
volatile E item;
volatile Node<E> next;
// ........
}
//头节点
private transient volatile Node<E> head;
//尾节点
private transient volatile Node<E> tail;
Node节点主要包含了两个域:一个是数据域item,另一个是next指针,用于指向下一个节点,从而构成链式队列,并且都是用volatile进行修饰的,以保证内存可见性。
ConcurrentLinkedQueue类有两个构造方法:
①默认构造方法,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,则直接抛出空指针异常
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;
}
默认情况下head节点存储的元素为空,tail节点等于head节点。
head = tail = new Node<E>(null);
2.入队操作offer
入队列就是将入队节点添加到队列的尾部。
上图所示的元素添加过程如下:
①添加元素1:队列更新head节点的next节点为元素1节点。又因为tail节点默认情况下等于head节点,所以它们的next节点都指向元素1节点。
②添加元素2:队列首先设置元素1节点的next节点为元素2节点,然后更新tail节点指向元素2节点,tail的next指向null。
③添加元素3:设置tail节点的next节点为元素3节点。
④添加元素4:设置元素3的next节点为元素4节点,然后将tail节点指向元素4节点,然后将tail的next指向null。
入队操作主要做两件事情,第一是将入队节点设置成当前队列尾节点的下一个节点。第二是更新tail节点,如果tail节点的next节点不为空,则将入队节点设置成tail节点;如果tail节点的next节点为空,则将入队节点设置成tail的next节点,所以tail节点不总是尾节点,理解这一点很重要。
这是从单线程入队的角度来理解入队过程,但是多个线程同时进行入队,情况就变得更加复杂,因为可能会出现其他线程插队的情况。如果有一个线程正在入队,那么它必须先获取尾节点,然后设置尾节点的下一个节点为入队节点,但这时可能有另外一个线程插队了,那么队列的尾节点就会发生变化,这时当前线程要暂停入队操作,然后重新获取尾节点。
来看看ConcurrentLinkedQueue的add入队方法:
public boolean add(E e) {
return offer(e);
}
public boolean offer(E e) {
// 创建入队节点,如果e为null,则直接抛出空指针异常
final Node<E> newNode = newNode( Objects.requireNonNull(e));
// 循环CAS直到入队成功:不断重试(for只有初始化条件,没有判断条件),直到将node加入队列
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next; // q一直指向p的下一个
//判断p是不是尾节点,q为null表示p是最后一个元素,尝试加入队列
if (q == null) {
//设置p节点的下一个节点为新节点,设置成功则casNext返回true;否则返回false,说明有其他线程更新过尾节点