本文纯粹为了解AQS框架与线程池后对队列知识原理的随手补充笔记。
前言
Java并发编程中总离不开线程池的管理和使用,而线程池的线程执行与阻塞机制只要依赖于其使用的队列。
Java并发包JUC下的队列主要分为以下两种:
- 单端队列:队列只有一个入口一个出口,单端队列类直接实现
Queue
接口,常见的线程池都会使用单端队列作为其线程队列 - 双端队列:支持在两端插入和删除元素的队列,继承了
Queue
接口,双端队列类直接实现该接口
JUC下常见的队列相关类简介如下:
BlockingQueue
接口:阻塞队列的抽象,不接受空元素,当队列已满存元素或队列空取元素会发生阻塞BlockingDeque
接口:继承了BlockingQueue
、Dequeue
接口,阻塞双端队列的抽象,不接受空元素,当队列已满存元素或队列空取元素会发生阻塞ArrayBlockingQueue
:基于数组实现的有界阻塞队列,由于队列的特性所以不适合有双端实现LinkedBlockingQueue
:基于链表节点的可选有界阻塞队列,SingleThreadPool与FixedThreadPool线程池使用的队列LinkedBlockingDeque
:基于链表节点的可选有界阻塞双端队列ScheduledThreadPoolExecutor.DelayedWorkQueue
:ScheduledThreadPoolExecutor类的内部类,ScheduledThreadPool线程池使用的延迟队列,根据任务执行时间排序基于堆的数据结构的延迟队列DelayQueue
:延迟任务的无限制阻塞队列SynchronousQueue
:一个没有内部容量的同步队列,每个插入操作必须等待另一个线程进行相应的删除操作,反之亦然。CachedThreadPool线程池使用的队列
ArrayBlockingQueue
ArrayBlockingQueue是基于数组实现的有界阻塞队列,此队列元素排序为FIFO,队列的头是在队列中等待时间最长的元素,队列的尾部是在队列中等待时间最短的元素。在队列的尾部插入新元素,队列检索操作在队列的头部获取元素。ArrayBlockingQueue的容量设置后就无法再更改,尝试将元素放入满了的队列将导致操作阻塞,从空队列中获取元素同样会阻塞。
ArrayBlockingQueue支持公平性策略,用于排序等待的生产者和消费者线程,默认为非公平策略,公平性设置为true的队列按FIFO顺序授予线程访问权。公平性通常会降低吞吐量,但会降低可变性并避免饥饿。
ArrayBlockingQueue主要的构造方法如下:
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
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();
}
ArrayBlockingQueue中的主要属性如下:
Object[] items
:元素数组int takeIndex
:下次取元素的数组索引int putIndex
:下次设置元素的数组索引int count
:队列元素数ReentrantLock lock
:队列锁Condition notEmpty
:表示队列状态不为空,取元素的等待队列Condition notFull
:表示队列状态未满,存元素的等待队列
LinkedBlockingQueue
LinkedBlockingQueue是基于链表节点的可选有界阻塞队列。该队列元素排序为FIFO,队列的头是在队列中等待时间最长的元素,队列的尾部是在队列中等待时间最短的元素,这方面跟ArrayBlockingQueue
相同。链接队列通常比基于数组的队列具有更高的吞吐量,但在大多数并发应用程序中的性能可预测性较差。
该队列默认构造的元素容量是无界(Integer.MAX_VALUE)的,一般建议使用有容量参数的方法构造,避免链表里元素过多导致OOM。
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
LinkedBlockingQueue中的主要属性如下:
Node<E> head
:链表头节点Node<E> last
:链表尾节点ReentrantLock takeLock
:元素取锁Condition notEmpty
:表示队列状态不为空,取元素的等待队列ReentrantLock putLock
:元素存锁Condition notFull
:表示队列状态未满,存元素的等待队列
看完以上属性相信都可以推导出队列的大致工作流程了,代码中的实现流程具体如下:
- 取元素:调用
take()
方法
1.使用takeLock
上锁,如果队列为空则使当前线程进入等待状态直到被notEmpty
唤醒,不为空则进入步骤2
2.元素出队,判断取出前队列元素数是否大于1,大于1则使用notEmpty
唤醒等待取元素的线程
3.takeLock
解锁
4.判断元素取出前队列数是否已满,如果未满则使用notFull
发出队列不满的信号唤醒存元素的等待线程
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
- 存元素:调用
offer(E)
方法
- 队列满就直接返回false,不空则进入步骤2
- 使用
putLock
上锁,元素入队,入队后如果队列未满使用notFull
发出未满信号使存元素的线程继续执行无需等待 putLock
解锁- 判断元素插入前队列数是否为0,如果为0则使用
notEmpty
发出队列不为空的信号唤醒取元素的等待线程
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
if (count.get() == capacity)
return false;
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
if (count.get() < capacity) {
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
}
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return c >= 0;
}