阻塞和非阻塞队列的并发安全原理是什么?
1、阻塞队列:ArrayBlockingQueue 源码分析
我们首先看一下 ArrayBlockingQueue 的源码,ArrayBlockingQueue 有以下几个重要的属性:
![](https://img2020.cnblogs.com/blog/593998/202108/593998-20210813120016854-13301404.png)
- 第一个就是最核心的、用于存储元素的 Object 类型的数组;然
- 后它还会有两个位置变量,分别是 takeIndex 和 putIndex,这两个变量就是用来标明下一次读取和写入位置的;
- 另外还有一个 count 用来计数,它所记录的就是队列中的元素个数。
另外,我们再来看下面这三个变量:
这3个变量也非常关键,
- 第一个就是一个 ReentrantLock,
- 下面两个 Condition 分别是由 ReentrantLock 产生出来的,
这三个变量就是我们实现线程安全最核心的工具。
ArrayBlockingQueue 实现并发同步的原理就是利用 ReentrantLock 和它的两个 Condition,读操作和写操作都需要先获取到 ReentrantLock 独占锁才能进行下一步操作。进行读操作时如果队列为空,线程就会进入到读线程专属的 notEmpty 的 Condition 的队列中去排队,等待写线程写入新的元素;同理,如果队列已满,这个时候写操作的线程会进入到写线程专属的 notFull 队列中去排队,等待读线程将队列元素移除并腾出空间。
下面,我们来分析一下最重要的 put 方法:
紧接着 ,是一个非常经典的 try finally 代码块,finally 中会去解锁,try 中会有一个 while 循环,它会检查当前队列是不是已经满了,也就是 count 是否等于数组的长度。
如果等于就代表已经满了,于是我们便会进行等待,直到有空余的时候,我们才会执行下一步操作,调用 enqueue 方法让元素进入队列,最后用 unlock 方法解锁。
你看到这段代码不知道是否眼熟,我在用 Condition 实现生产者/消费者模式的时候,写过一个 put 方法,代码如下:
和 ArrayBlockingQueue 类似,其他各种阻塞队列如 LinkedBlockingQueue、PriorityBlockingQueue、DelayQueue、DelayedWorkQueue 等一系列 BlockingQueue 的内部也是利用了 ReentrantLock 来保证线程安全,只不过细节有差异,比如: LinkedBlockingQueue 的内部有两把锁,分别锁住队列的头和尾,比共用同一把锁的效率更高,不过总体思想都是类似的。
阻塞的原理
写时的阻塞
因为写入时阻塞主要是put方法,所以可以通过两个实现类的put方法来看一下是如何实现。
- ArrayBlockingQueue
![](https://img2020.cnblogs.com/blog/593998/202108/593998-20210820163351987-1740228485.png)
可以看到这里会获取到一个锁,然后在在入队之前会有一个while,条件是count==item.length,其中count是指的当前队列已经写入的数据项个数,item是用于存数据的一个数组。
也就是说如果当前队列的数据项等于数组的长度了,说明已经满了,此时则调用noteFull.await()阻塞当前线程;
读时的阻塞
读时的配对方法是take,这个方法会对读取进行阻塞。
2、非阻塞队列:ConcurrentLinkedQueue
看完阻塞队列之后,我们就来看看非阻塞队列 ConcurrentLinkedQueue。顾名思义,ConcurrentLinkedQueue 是使用链表作为其数据结构的,我们来看一下关键方法 offer 的源码:
可以看出,非阻塞队列 ConcurrentLinkedQueue 使用 :CAS 非阻塞算法 + 不停重试,来实现线程安全,适合用在不需要阻塞功能,且并发不是特别剧烈的场景。
总结
本阻塞队列最主要是利用了 ReentrantLock 以及它的 Condition 来实现,而非阻塞队列则是利用 CAS 方法实现线程安全。