ArrayBlockingQueue LinkedBlockingQueue阻塞队列
前言
ArrayBlockingQueue和LinkedBlockingQueue都是JUC包下的阻塞队列,只是实现的方式不同,我们都清楚阻塞队列被运用在线程池中,用来存储要被执行的任务,除了被运用在线程池中,如果有场景需要保证任务被顺序执行,也可以单独的使用到阻塞队列,因为队列具有先入先出(FIFO)的特点
ArrayBlockingQueue主要实现代码结构
public class ArrayBlockingQueue extends AbstractQueue<E>
implements BlockingQueue<E>,java.io.Serializable {
/** 存储元素的数组结构 */
final Object[] items;
/** 出队的下标索引 */
int takeIndex;
/** 入队的下标索引 */
int putIndex;
/** 储存的元素数量 */
int count;
/** 控制入队和出队操作的可重入锁 */
final ReentrantLock lock;
/** 非空条件锁 */
private final Condition notEmpty;
/** 非满条件锁 */
private final Condition notFull;
}
可以从主要实现的代码结构上看出:
1. 采用数组储存元素
2. 出队和入队都具有下标索引,控制着出队和入队的位置
3. 使用一把ReentrantLock可重入锁来控制着入队和出队操作的并发访问
4. 使用两个Condition条件锁来达到线程的等待和唤醒,当出队队列中的元素为空则调用notEmpty.await()进行等待,注意await()会释放当前锁,当有元素入队时,调用notEmpty.signal()唤醒前面等待的线程,注意signal()会重新获得锁,入队操作类似
构造方法
public ArrayBlockingQueue(int capacity, boolean fair,
Collection<? extends E> c) {
this(capacity, fair);
final ReentrantLock lock = this.lock;
// 注意的点,这里加锁,保证items的可见性,因为存在指令重排序问题,加锁完成对items数组的初始化
lock.lock();
try {
int i = 0;
try {
for (E e : c) {
checkNotNull(e);
items[i++] = e;
}
} catch (ArrayIndexOutOfBoundsException ex) {
throw new IllegalArgumentException();
}
count = i;
putIndex = (i == capacity) ? 0 : i;
} finally {
lock.unlock();
}
}
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
// 需要预先分配容量
this.items = new Object[capacity];
// 支持公平锁,非公平锁,默认非公平锁
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
注意的点:
1. 必须传入数组初始容量,对JVM比较友好,不需要后面动态扩展数组内存大小
2. 支持公平锁,非公平锁
3. 如果传入了collection集合,则需要加锁完成对items数组的初始化,保证items的可见性,因为存在指令重排序问题(这里不在详细描述了)
入队操作
// 最终还是调用offer(E e)进行入队
public boolean add(E e) {
return super.add(e);
}
// offer(E e)入队操作
public boolean offer(E e) {
checkNotNull(e);
// 进行加锁操作
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 如果队列已满,则返回false入队失败,不会进行notFull.await()等待操作
if (count == items.length)
return false;
else {
// 真正执行入队操作,将元素放到items中去
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
// 还提供了offer(E e, long timeout, TimeUnit unit)超时等待方法
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
checkNotNull(e);
long nanos = unit.toNanos(timeout);
// 加锁,支持锁中断
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 队列已满,则等待nanos纳秒
// 如果唤醒这个线程时依然没有空间且时间到了就返回false
while (count == items.length) {
if (nanos <= 0)
return false;
nanos = notFull.awaitNanos(nanos);
}
// 真正执行入队操作
enqueue(e);
return true;
} finally {
lock.unlock();
}
}
// 提供了put(E e)入队操作,一直等待直到入队
public void put(E e) throws InterruptedException {
checkNotNull(e);
// 加锁,支持锁中断
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 队列已满,则调用notFull.await()等待,释放锁,重新被唤醒时,再获取到锁
// 被唤醒时,再次判断队列是否已满,还是已满,则还是等待
// 直到入队完成
while (count == items.length)
notFull.await();
// 入队
enqueue(e);
} finally {
lock.unlock();
}
}
// 无论是offer还是put入队操作,调用的入队方法都是enqueue(E x)
private void enqueue(E x) {
final Object[] items = this.items;
// 将元素放到数组的入队下标索引
items[putIndex] = x;
// 判断入队下标索引是否达到数组长度,如果已经达到,则返回数组头部
if (++putIndex == items.length)
putIndex = 0;
// 数量+1
count++;
// 唤醒非空的条件锁,需要唤醒之前等待出队的线程
notEmpty.signal();
}
整体来说,入队操作的方法还是比较简单易懂的,需要注意的点是当队列已满,需要调用notFull.await()等待操作,当有元素从队列出队时,调用notFull.signal()唤醒操作
出队操作
public E poll() {
// 加锁操作
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 判断队列是否为空
// 如果队列为空,则直接返回null,不会调用notEmpty.await()等待操作
// 如果队列不为空,则调用dequeue出队操作
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
// 还提供poll(long timeout, TimeUnit unit)出队超时等待
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 如果队列无元素,则等待nanos纳秒
// 如果下一次线程被唤醒但队列依然无元素且已超时就返回null
while (count == 0) {
if (nanos <= 0)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
// 执行出队操作
return dequeue();
} finally {
lock.unlock();
}
}
// 提供了take()出队操作,一直等待直到有元素出队
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
// 无论是poll还是take出队操作,调用的出队方法都是dequeue(),完成出队
private E dequeue() {
final Object[] items = this.items;
// 获取到数组的出队下标索引位置的元素
E x = (E) items[takeIndex];
// 将数组的出队下标索引位置至为null
items[takeIndex] = null;
// 判断出队下标索引是否达到数组长度,如果已经达到,则返回数组头部
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
// 唤醒非满的条件锁,需要唤醒之前等待入队的线程
notFull.signal();
return x;
}
ArrayBlockingQueue主要的代码实现还是比较容易看懂的,基本上围绕着加锁,条件锁的等待和唤醒,数组下标的循环利用
LinkedBlockingQueue主要实现代码结构
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
/** 链表容量 */
private final int capacity;
/** 链表元素的数量,需要采用原子类CAS操作完成++,--操作 */
private final AtomicInteger count = new AtomicInteger();
/** 链表的头节点,表示出队的指针 */
transient Node<E> head;
/** 链表的尾节点,表示入队的指针 */
private transient Node<E> last;
/** 用于控制出队的ReentrantLock可重入锁 */
private final ReentrantLock takeLock = new ReentrantLock();
/** 非空条件锁 从takeLock中获取*/
private final Condition notEmpty = takeLock.newCondition();
/** 用于控制入队的ReentrantLock可重入锁 */
private final ReentrantLock putLock = new ReentrantLock();
/** 非满条件锁 从putLock中获取 */
private final Condition notFull = putLock.newCondition();
}
// 链表的节点Node类
static class Node<E> {
// 节点的value元素
E item;
// 下一个节点
Node<E> next;
Node(E x) { item = x; }
}
从主要的实现代码结构上可以看出:
1. 采用链表储存元素
2. 具有头部和尾部指针,表示要出队和入队的节点Node位置
3. 队列的数量采用原子类AtomicInteger, 完成计算
4. 使用两把锁ReentrantLock分别控制出队和入队操作,出队和入队两个操作并不互斥,这也是跟ArrayBlockingQueue队列的最主要的区别,也是为什么count要采用原子类AtomicInteger进行计算了,因为入队和出队可以一起执行
构造方法
public LinkedBlockingQueue(Collection<? extends E> c) {
// 链表默认的大小为 Integer.MAX_VALUE
this(Integer.MAX_VALUE);
// 锁住入队操作,完成节点的初始化
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
int n = 0;
for (E e : c) {
if (e == null)
throw new NullPointerException();
if (n == capacity)
throw new IllegalStateException("Queue full");
enqueue(new Node<E>(e));
++n;
}
count.set(n);
} finally {
putLock.unlock();
}
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
注意的点:
1. 可以指定链表的容量,如果不指定则默认为Integer.MAX_VALUE
2. 两个ReentrantLock都是非公平锁,不支持公平锁
3. 传入collection集合,完成链表的初始化,需要putLock加锁控制完成对链表的初始化
入队操作
这里大概主要讲一下put的具体操作,offer的入队操作类似
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
Node<E> node = new Node<E>(e);
// putLock入队锁 加锁控制
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
// 如果链表元素已满,则调用notFull.await()进行等待
while (count.get() == capacity) {
notFull.await();
}
// 入队操作
enqueue(node);
// 链表数量通过CAS操作+1
c = count.getAndIncrement();
// 如果数量 < 容量 调用notFull.signal()唤醒等待入队的线程
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
// 表明之前队列是空,可能会存在等待出队的线程,这时候已经入队一个元素,则需要唤醒
// c = count.getAndIncrement(); 得到的是还未累加之前的结果
if (c == 0)
// 唤醒等待出队的线程
signalNotEmpty();
}
// 入队操作
private void enqueue(Node<E> node) {
// 直接将尾部节点的next指针指向node节点
last = last.next = node;
}
可以看出,LinkedBlockingQueue入队操作通过putLock加锁控制,这时不影响到出队操作,count累加采用CAS操作进行累加
出队操作
重点分析下take()方法,poll出队操作类似
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
// takeLock出队锁,进行加锁
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
// 如果队列为空,则需要进行等待
while (count.get() == 0) {
notEmpty.await();
}
// 出队
x = dequeue();
// 调用CAS操作进行递减
c = count.getAndDecrement();
// 如果递减之前的数量大于1,表明队列中还存在元素,则需要进行唤醒之前等到出队的线程
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
// 表明之前队列已满,可能会存在等待入队的线程,这时候已经出队一个元素,则需要唤醒
if (c == capacity)
// 唤醒入队线程
signalNotFull();
return x;
}
// 真正的出队操作
private E dequeue() {
// h -> head头部节点,头部节点不存放数据
Node<E> h = head;
// 头部节点的下一个节点,第二个节点
Node<E> first = h.next;
// 头部节点的next指针指向自己,也就是断开与第二个节点的链接
h.next = h;
// 再让head指向第二个节点
head = first;
E x = first.item;
// 将头部节点的item至为null,因为头部节点不存在数据
first.item = null;
return x;
}
可以看出入队和出队的源码还是比较简单易懂的,跟ArrayBlockingQueue的出队和入队的操作类似,只不过ArrayBlockingQueue采用数组存储元素,而LinkedBlockingQueue采用链表存储元素,ArrayBlockingQueue加锁控制使用一把锁,LinkedBlockingQueue则采用两把锁去分别控制出队和入队的
ArrayBlockingQueue和LinkedBlockingQueue的区别
从上面的分析可以得出他们之间最主要的区别有以下几点:
1. ArrayBlockingQueue采用数组进行存储,需要预先指定数组容量,对JVM比较友好,不需要后面JVM为其动态扩展其容量。LinkedBlockingQueue采用链表进行存储,储存元素的时候需要额外构造出Node节点,不需要预先指定链表大小,默认为Integer.MAX_VALUE,后面JVM需要为其动态的扩展内存空间
2. ArrayBlockingQueue的入队和出队操作都采用一把ReentrantLock锁去控制,而LinkedBlockingQueue的入队和出队操作采用两把ReentrantLock锁去控制,入队和出队操作不互斥
3. 在具有大量的出队和入队操作的场景下,ArrayBlockingQueue的性能要低于LinkedBlockingQueue的性能,就第2点讲的,ArrayBlockingQueue的出队和入队是互斥的,而LinkedBlockingQueue可以一边出队一边入队
前面讲到在高并发场景下,ArrayBlockingQueue的性能要低于LinkedBlockingQueue,是因为ArrayBlockingQueue只采用一把锁去控制入队和出队的原因,那么是否ArrayBlockingQueue也能像LinkedBlockingQueue那样采用两把锁分别控制入队和出队操作了????
我觉得也是可以的,ArrayBlockingQueue也可以做到像LinkedBlockingQueue那样采用两把锁去控制,如果采用两把锁去控制的话,那么ArrayBlockingQueue的count也要做成原子类AtomicInteger进行累加,累减操作。还有一点是LinkedBlockingQueue因为是链表储存,在入队时需要额外的构造新的Node节点,出队时需要改变Node节点的指针,效率没有ArrayBlockingQueue的数组对象来的快,所以LinkedBlockingQueue采用两把锁去提升效率,而在并发量不高的情况下,ArrayBlockingQueue的性能可能要比LinkedBlockingQueue高一些,因为ArrayBlockingQueue的不需要去操作额外的Node节点数据,count操作没有采用原子类的CAS操作,采用原生的++,--操作
总结
ArrayBlockingQueue和LinkedBlockingQueue都是一种阻塞队列,只不过对应的实现方式而已,一个采用数组,一个采用链表,一个使用一把ReentrantLock锁去控制入队和出队操作,另外一个使用两把ReentrantLock锁去分别控制入队和出队操作,要选用那种队列的话,需要根据具体的业务场景去使用,像阻塞队列的话,JDK提供了7种阻塞队列,这只是其两种,剩下的5种:
- PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
- DelayQueue:一个使用优先级队列实现的无界阻塞队列。
- SynchronousQueue:一个不存储元素的阻塞队列。
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。