要实现一个线程安全的队列有两个方式:一种是使用阻塞算法,另一种是使用非阻塞算法。
阻塞算法:
使用阻塞算法的队列可以用一个锁(入队和出队同一把锁)或两把锁(入队和出队用不同的锁)来实现。
非阻塞的实现方式则可以使用循环CAS的方式来实现。
ConcurrentLinkedQueue非阻塞线程安全队列
ConcurrentLinkedQueue是一个基于链接节点的无界 线程安全的队列。
它采用FIFO(先进先出)的规则对节点进行排序;
当我们添加一个元素的时候,它会添加到队列的尾部;
当我们获取一个元素时,它会返回队列头部的元素。
队列中不允许null元素。
判断队列成员是否为空,不要使用size()方法,使用isEmpty()方法,因为size()方法会遍历整个队列。
ConcurrentLinkedQueue由head节点和tail节点组成,每个节点(Node)由节点元素(item)和指向下一个节点的引用(next)组成;
节点与节点之间就是通过这个next关联起来,从而形成一个链表结构的队列。
我们看下源码中的参数:
private transient volatile Node<E> head;
private transient volatile Node<E> tail;
public ConcurrentLinkedQueue() {
head = tail = new Node<E>(null);
}
要是你看过HahsMap的源码,会发现,这个和Entry链表差不多..
默认情况下head节点存储的元素为空,tail节点等于head节点
我们看下Node的源码:
private static class Node<E> {
volatile E item;
volatile Node<E> 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);
}
boolean casItem(E cmp, E val) {
return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
}
void lazySetNext(Node<E> val) {
UNSAFE.putOrderedObject(this, nextOffset, val);
}
boolean casNext(Node<E> cmp, Node<E> val) {
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
.....
}
入队列就是将新添加的Node节点,添加到队列的尾部。
在没添加元素之前,head和tail都是Node自己(head节点)。
a. 添加元素1:队列更新head节点的next节点为"元素1节点",又因为tail节点默认情况下等于head节点,所以他们的next节点都指向"元素1节点"
b. 添加元素2:队列首先设置"元素节点1"的next节点为"元素2节点",然后更新tail节点指向"元素2节点"
c. 添加元素3:设置tail节点为元素3节点
d. 添加元素4:设置元素3的next节点为元素4节点,然后将tail节点指向元素4节点。
通过观察入队过程以及head节点和tail节点的变化,发现入队主要做两件事:
第一:将入队节点设置成当前队列尾节点的下一个节点。
第二:更新tail节点,如果tail节点的next节点不为空,则将入队节点设置成tail节点,如果tail节点的next节点为空,则将入队节点设置成tail的next节点,所以tail节点不总是尾节点。(理解这点很重要...)
(让tail节点永远作为队列的尾节点,这样实现的代码量非常少,而且逻辑清晰、易懂,但是有个缺点,每次都要使用循环CAS更新tail节点。
如果能减少CAS更新tail节点的次数,就能提高入队的效率;so,并不是每次节点入队后都将tail节点更新成尾节点;而且随着队列长度越来越大,
每次入队时定位尾节点的时间就越长,太消耗性能..)
我们来看下源码:
public boolean offer(E e) {
// 检查传入参数是否为空
checkNotNull(e);
// 构造一个新的Node节点,入队节点
final Node<E> newNode = new Node<E>(e);
// t为tail节点,p为尾节点,默认相等,采用失败即重试的方式,直到入队成功
for (Node<E> t = tail, p = t;;) {
// 获取p的下一个节点q
Node<E> q = p.next;
// 如果q为null(p就是尾节点)
if (q == null) {
// p is last node,p是尾节点
// 将入队节点newNode,设置为当前队列尾节点的next节点
if (p.casNext(null, newNode)) {
// 判断tail节点是不是尾节点,也可以理解为如果插入节点后,tail节点与p节点距离是否达到两个节点?
if (p != t) // hop two nodes at a time
// 如果tail不是尾节点,则将入队列节点(newNoed)设置为tail节点。
casTail(t, newNode); // Failure is OK.
return true;
}
}
// 如果p和它的下一个节点相等。
// 则说明p节点和q节点都为空,表示队列刚刚初始化,所以返回head节点
else if (p == q)
p = (t != (t = tail)) ? t : head;
else
// p有next节点,表示p的next节点是尾节点,则需要重新更新p后,将它指向next节点
p = (p != t && t != (t = tail)) ? t : q;
}
}
(此源码来自JDK1.7,与原书有差别)
从源码角度来看,整个入队过程主要做两件事:
1. 定位出尾节点;
tail节点并不总是尾节点,所以每次入队都必须通过tail节点来找到尾节点。
尾节点可能是tail节点也可能是tail节点的next节点
2. 使用CAS算法将入队节点设置成尾节点的next节点,如不成功则重试。
ConcurrentLinkedQueue出队列
出队列就是从队列中返回一个节点元素,并清空该节点对元素的引用。
观察上图可以发现,并不是每次出队列时都更新head节点,当head节点有元素时,直接弹出head节点中的元素,而不会更新head节点。
只有当head节点中没有时,出队操作才会更新head节点。
主要是,尽量减少CAS更新head节点的消耗,这种做法可以提高出队的效率。
ConcurrentLinkedQueue使用Demo
public class ConcurrentLinkedQueueTest {
private static ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<Integer>();
private static int count = 2;
private static CountDownLatch latch = new CountDownLatch(count);
public static void main(String[] args) throws Exception {
long startTime = System.currentTimeMillis();
ExecutorService service = Executors.newFixedThreadPool(4);
ConcurrentLinkedQueueTest.offer();
for (int i = 0; i < count; i++) {
service.submit(new Pool());
}
latch.await();
System.out.println("cost time : " + (System.currentTimeMillis() - startTime) + " ms");
service.shutdown();
}
/**
* 生产者
*/
public static void offer() {
for (int i = 0; i < 10000; i++) {
queue.offer(i);
}
}
/**
* 消费
*
* @author CYX
* @time 2017年7月31日上午9:34:02
*/
static class Pool implements Runnable {
@Override
public void run() {
// 此处判断队列是否为空,不要使用size(),size()会将队列先遍历一遍,性能太差
while (!queue.isEmpty()) {
System.out.println("消费 : " + queue.poll());
}
latch.countDown();
}
}
}