java并发队列之非阻塞队列ConcurrentLinkedQueue(七)
ConcurrentLinkedQueue是一个非阻塞,无界的高并发队列.底层数据结构使用单链表来实现,出队和入队操作使用CAS来实现线程安全.
从图中可以看出非阻塞队列和阻塞队列非常像,只是非阻塞队列并未实现BlackingQueue接口.
实战
public class ConcurrentLinkedQueueDemo {
public static void main(String[] args) {
final ConcurrentLinkedQueue<String> deque = new ConcurrentLinkedQueue<>();
final
Runnable producerRunnable = new Runnable() {
int i = 0;
public void run() {
while (true) {
i++;
try {
System.out.println("我生产了一个===" + i);
deque.add(i + "dddd");
//为了凸显非阻塞队列的特性,这里时间设置长一点,而take的等待时间设置短一点,来看看poll是非阻塞的.
//两边的时间也可以对调来检验put是非阻塞的.
//注意和前面的阻塞队列做对比哦.
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Runnable customerRunnable = new Runnable() {
public void run() {
while (true) {
try {
System.out.println("我消费了一个===" + deque.poll());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread thread1 = new Thread(producerRunnable);
thread1.start();
Thread thread2 = new Thread(customerRunnable);
thread2.start();
}
}
输出结果:
我生产了一个===1
我消费了一个===1dddd
我消费了一个===null
我生产了一个===2
我消费了一个===2dddd
我消费了一个===null
我生产了一个===3
我消费了一个===3dddd
我消费了一个===null
我生产了一个===4
我消费了一个===4dddd
我消费了一个===null
我生产了一个===5
我消费了一个===5dddd
我消费了一个===null
我生产了一个===6
我消费了一个===6dddd
我消费了一个===null
注意结果和前面的阻塞队列做对比哦.
结果解说:可以看到非阻塞队列并没有take方法(阻塞).在阻塞队列中,调用take时如果队列里面没有元素则会阻塞知道队列里面有元素.而非阻塞第一是没有take阻塞方法,第二是在poll获取元素时,发现没有元素并不会去阻塞线程,而是直接返回null.
ConcurrentLinkedQueue特性
- 没有实现BlockingQueue,也就没有了阻塞方法put,take.
- 队列尾添加元素,队列头获取元素.
- 没有像阻塞队列里面使用Lock锁来保证安全性.使用CAS操作保证安全性,性能较高.
- 内部使用单链表结构,无界,无阻塞.
源码解析
入栈操作offer
private transient volatile Node<E> head;
private transient volatile Node<E> tail;
//初始化时,创建了一个null节点,头尾都指向它
public ConcurrentLinkedQueue() {
head = tail = new Node<E>(null);
}
public boolean offer(E e) {
checkNotNull(e);
//构造Node 节点,在构造函数内部调用unsafe.putObject来添加元素
final Node<E> newNode = new Node<E>(e);
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
if (q == null) {
// 如果尾节点为null则执行新增插入动作.
//cas操作
if (p.casNext(null, newNode)) {
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)
//防止多线程操作时,另外一个节点poll操作移除之后,head变成自引用.
//重新找新的head
p = (t != (t = tail)) ? t : head;
else
//寻找尾节点
p = (p != t && t != (t = tail)) ? t : q;
}
}
poll弹出队列操作
public E poll() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
if (item != null && p.casItem(item, null)) {
//出队列后,更新头节点往下移动.等同于删除一个元素
if (p != h) // hop two nodes at a time
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
总结:ConcurrentLinkedQu eue 的底层使用单向链表数据结构来保存队列元素,每个元素被包装成一个Node 节点。队列是靠头、尾节点来维护的,创建队列时头、尾节点指向-个item 为null 的哨兵节点。第一次执行peek 或者自rst 操作时会把head 指向第一个真正的队列元素。由于使用非阻塞CAS 算法,没有加锁,所以在计算size 时有可能进行了offer 、poll 或者remove 操作, 导致计算的元素个数不精确,所以在井发情况下size 函数不是很有用。