文章目录
- 1. LinkedBlockingQueue 简介
- 2. LinkedBlockingQueue 的关键属性
- 3. LinkedBlockingQueue 的元素存取流程
- 3.1 添加元素
- 3.2 取出元素
1. LinkedBlockingQueue 简介
LinkedBlockingQueue
是线程池默认使用的任务队列,为了满足多线程环境下元素出入队列的安全性,其主要有以下特点:
LinkedBlockingQueue
是基于链表结构的阻塞队列,默认容量为Integer.MAX_VALUE
,可以认为是无界队列LinkedBlockingQueue
队列按 FIFO(先进先出)排序元素LinkedBlockingQueue
内部使用两个独占锁来保证线程安全,其中写入锁用于控制添加元素的操作,取出锁用于控制取出元素的操作。新元素从队列尾部入队,取出元素则从队列头部移除,因此可以同时在队列头部和队列尾部并发地进行元素取出、添加操作
2. LinkedBlockingQueue 的关键属性
LinkedBlockingQueue
中添加到队列的数据都将被封装成 Node
节点,可以看到 Node
的结构比较简单,只有一个后指针指向下一个节点,形成单向链表结构
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
LinkedBlockingQueue
中关键属性如下,其中 head
和 last
分别指向队列的头节点和尾节点。LinkedBlockingQueue
内部分别使用 takeLock
和 putLock
两个独占锁对并发进行控制,在高并发的情况下生产者和消费者可以并行地操作队列中的数据,也就大大提高了队列的并发性能
// 最大容量上限,默认是 Integer.MAX_VALUE
private final int capacity;
// 元素计数器,这是个原子类。因为读写分别使用不同的锁,但都会访问这个属性,所以它需要保证线程安全
private final AtomicInteger count = new AtomicInteger();
// 队列头结点
private transient Node<E> head;
// 队列尾结点
private transient Node<E> last;
// 队头出队锁
private final ReentrantLock takeLock = new ReentrantLock();
// 等待获取出队锁的线程的等待队列
private final Condition notEmpty = takeLock.newCondition();
// 队尾入队锁
private final ReentrantLock putLock = new ReentrantLock();
// 等待获取入队锁的线程的等待队列
private final Condition notFull = putLock.newCondition();
3. LinkedBlockingQueue 的元素存取流程
3.1 添加元素
LinkedBlockingQueue
添加元素的主要方法是 LinkedBlockingQueue#put()/LinkedBlockingQueue#offer()
,本文以 LinkedBlockingQueue#put()
为例
LinkedBlockingQueue#put()
方法的处理步骤如下:
- 当前线程首先获取 putLock 写入锁
- 获取锁成功,则判断当前队列是否已经满了,队列容量已满的情况下不允许添加新的元素,此时当前线程需要挂起等待,
notFull.await()
类似于Object.wait()
方法- 线程唤醒后依然在 while 循环中判断队列容量是否已满,因为消费线程会不断取出队列中的元素,故此处循环正常情况下是一定能够跳出的。跳出后调用
LinkedBlockingQueue#enqueue()
方法将元素入队- 当前线程完成添加元素的操作,检查队列容量是否已满,如果队列没有达到容量上限显然可以继续添加元素,则将在等待 putLock 写入锁的一个线程唤醒,让它能完成添加元素的操作,
notFull.signal()
类似于Object.notify()
方法,是 AQS 的一种实现机制,- 当前线程释放 putLock 写入锁
- 如果队列在本次添加元素之前是空的,那么很有可能存在消费线程挂起等待可消费的元素,故此时调用
LinkedBlockingQueue#signalNotEmpty()
方法唤醒一个在等待的消费线程
LinkedBlockingQueue#put()
方法会一直阻塞直到能够把元素添加到队列中,也就是除非发生异常,否则元素是必然能添加到队列中的LinkedBlockingQueue#offer()
方法则允许添加元素失败,无法将元素添加到队列时直接返回 false 即可结束调用
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
LinkedBlockingQueue#enqueue()
方法的处理如下,非常简单易懂,就是元素从队列尾部入队
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node;
}
3.2 取出元素
LinkedBlockingQueue
取出元素的主要方法为 LinkedBlockingQueue#take()/LinkedBlockingQueue#poll()
,本文以 LinkedBlockingQueue#take()
为例
LinkedBlockingQueue#take()
方法的处理步骤如下:
- 当前线程首先获取 takeLock 取出锁
- 获取锁成功,则判断当前队列是否是空的,队列元素数量为 0 的情况下无法取出元素,此时当前线程需要挂起等待,调用
notEmpty.await()
方法- 线程唤醒后依然在 while 循环中判断队列是否为空,因为生产线程会往队列中添加元素,故此处循环是一定能够跳出的。跳出后调用
LinkedBlockingQueue#dequeue()
方法将队列头部元素出队- 元素出队后,检查队列是否是空的,如果队列不为空显然可以继续取出元素,则将在等待 takeLock 取出锁的一个线程唤醒,让它能完成取出元素的操作,调用
notEmpty.signal()
方法- 当前线程释放 takeLock 取出锁
- 如果队列在本次取出元素之前是满的,那么很有可能存在生产线程挂起等待时机添加元素,故此时调用
LinkedBlockingQueue#signalNotFull()
方法唤醒一个在等待的生产线程
LinkedBlockingQueue#take()
方法会一直阻塞直到能够从队列中取到元素,也就是除非发生异常,否则该方法是必然能获取到元素的LinkedBlockingQueue#poll()
方法则允许获取元素失败,无法从队列中获取元素时直接返回 null 即可结束调用
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;
}
LinkedBlockingQueue#dequeue()
方法其实就是将元素从队列头部移除,使其出队
private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
}