在上一篇文章中我们学习了阻塞队列(BlockingQueue)的实现原理。我们知道要实现一个线程安全的队列有两种实现方式一种是使用阻塞算法,另一种是使用非阻塞算法。阻塞队列就是通过使用加锁的阻塞算法实现的,而非阻塞的实现方式则可以使用循环CAS(如原子类Atomic)的方式来实现,本文我们一起如何使用非阻塞的方式来实现线程安全队列ConcurrentLinkedQueue的。
1. ConcurrentLinkedQueue的介绍
ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。它采用了“wait-free”算法来实现,该算法在Michael & Scott算法上进行了一些修改, Michael & Scott算法的详细信息可以参见参考资料一。
2. ConcurrentLinkedQueue的结构
我们通过ConcurrentLinkedQueue的类图来分析一下它的结构。
![](https://i-blog.csdnimg.cn/blog_migrate/c57e0b502016a78ce1cc944968dc88dc.jpeg)
(图1)
ConcurrentLinkedQueue由head节点和tair节点组成,每个节点(Node)由节点元素(item)和指向下一个节点的引用(next)组成,节点与节点之间就是通过这个next关联起来,从而组成一张链表结构的队列。默认情况下head节点存储的元素为空,tair节点等于head节点。
1 |
private transient volatile Node<e> tail = head; |
3. 入队列
入队列就是将入队节点添加到队列的尾部。为了方便理解入队时队列的变化,以及head节点和tair节点的变化,每添加一个节点我就做了一个队列的快照图。
![](https://i-blog.csdnimg.cn/blog_migrate/08e94be016caee86f6dd59cd98fbf949.jpeg)
(图二)
- 第一步添加元素1。队列更新head节点的next节点为元素1节点。又因为tail节点默认情况下等于head节点,所以它们的next节点都指向元素1节点。
- 第二步添加元素2。队列首先设置元素1节点的next节点为元素2节点,然后更新tail节点指向元素2节点。
- 第三步添加元素3,设置tail节点的next节点为元素3节点。
- 第四步添加元素4,设置元素3的next节点为元素4节点,然后将tail节点指向元素4节点。
通过debug入队过程并观察head节点和tail节点的变化,发现入队主要做两件事情,
第一是将入队节点设置成当前队列尾节点的下一个节点。
第二是更新tail节点,如果tail节点的next节点不为空,则将入队节点设置成tail节点,如果tail节点的next节点为空,则将入队节点设置成tail的next节点,所以tail节点不总是尾节点,理解这一点对于我们研究源码会非常有帮助。
上面的分析让我们从单线程入队的角度来理解入队过程,但是多个线程同时进行入队情况就变得更加复杂,因为可能会出现其他线程插队的情况。如果有一个线程正在入队,那么它必须先获取尾节点,然后设置尾节点的下一个节点为入队节点,但这时可能有另外一个线程插队了,那么队列的尾节点就会发生变化,这时当前线程要暂停入队操作,然后重新获取尾节点。让我们再通过源码来详细分析下它是如何使用CAS算法来入队的。
01 |
public boolean offer(E e) {
|
03 |
if (e == null ) throw new NullPointerException(); |
07 |
Node</e><e> n = new Node</e><e>(e); |
23 |
for ( int hops = 0 ; ; hops++) {
|
27 |
Node</e><e> next = succ(p); |