LinkedBlockingQueue源码解析
队列是很重要的API,线程池、读写锁、消息队列等技术和框架的底层原理都是队列,是很多高级API的基础。
LinkedBlockingQueue可以理解为一个典型的生产者-消费者模型。
1.整体架构
LinkedBlockingQueue,链表阻塞队列,底层的数据结构时链表,且队列是可阻塞的。
Queue接口和BlockingQueue接口
从类关系图中可以看出两条线路,
线路1
AbstractQueue --> AbstractCollection --> Collection --> Iterable
这条路线主要是想复用Collection和迭代器的一些操作。
线路2
BlockingQueue --> Queue -->Collection
Queue是最基础的接口,几乎所有队列实现类都实现这个接口,该接口定义了队列的三类基本操作,
- 新增操作
add方法队列满时抛出异常
offer方法队列满时返回false - 查看并删除操作
remove方法队列为空时返回false
poll方法队列为空时返回null - 只查看但不删除操作
element方法队列为空时抛出异常
peek方法队列为空时返回null
BlockingQueue在Queue的基础上添加了阻塞的概念,如一直阻塞或阻塞指定时间,
操作类型 | 抛异常 | 特殊值 | 阻塞 | 阻塞一段时间 |
---|---|---|---|---|
新增直至队列已满 | add | offer返回false | put | offer设置超时,超时返回false |
弹出队列头至队列为空 | 无 | remove返回false;poll返回null | take | poll设置超时,超时返回null |
查看队列头但不弹出 | element | peek返回null | 无 | 无 |
类注释
- 基于链表的阻塞队列,底层数据结构是链表
- 链表维护先入先出队列,新元素添加到队尾,获取元素时从头部取出
- 链表的大小在初始化时候可以设置,默认是Integer的最大值
- 可以使用Collection和Iterable的所有操作
内部结构
LinkedBlockingQueue可以分为三个部分,存储链表+锁+迭代器,
// 链表节点
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
// 链表容量,默认情况下是 Integer.MAX_VALUE
private final int capacity;
// 使用原子性的Integer类对象记录链表元素数目
private final AtomicInteger count = new AtomicInteger();
// 链表头
transient Node<E> head;
// 链表尾
private transient Node<E> last;
// 获取元素时候的锁,take和poll方法会用到
private final ReentrantLock takeLock = new ReentrantLock();
// 获取元素时的条件队列,可以理解为 等待链表not empty时才获取元素
private final Condition notEmpty = takeLock.newCondition();
// 添加元素时候的锁,put和offer方法会用到
private final ReentrantLock putLock = new ReentrantLock();
// 添加元素时的条件队列,可以理解为 等待链表not Full时才添加元素
private final Condition notFull = putLock.newCondition();
// 内部实现的迭代器
private class Itr implements Iterator<E> {
......
}
三种结构各司其职,
- 链表作用是保存当前节点,节点使用了泛型,所以节点中的数据可以是任意的对象。
- 锁有take和put锁,目的是保证队列操作时线程安全,同时take和put操作可以同时进行,互不影响。
初始化
LinkedBlockingQueue有三种初始化方式,
// 无参数初始化,默认容量是Integer.MAX_VALUE
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); // 注意,head节点一定是固定的值为null的节点对象,保证了不会空指针异常
}
// 使用集合对象初始化
public LinkedBlockingQueue(Collection<? extends E> c) {
this(Integer.MAX_VALUE);
final ReentrantLock putLock = this.putLock;
putLock.lock(); // Never contended, but necessary for visibility
try {
int n = 0;
for (E e : c) {
if (e == null)
throw new NullPointerException(); // 集合类元素不能为null
if (n == capacity)
throw new IllegalStateException("Queue full");
enqueue(new Node<E>(e));
++n;
}
count.set(n);
} finally {
putLock.unlock();
}
}
初始化源码中包含的信息,
- 初始化时容量大小不影响性能,只影响以后的使用,初始化队列太小会导致过早报出IllegalStateException异常
- 源码中for循环的形式并不优雅,添加一个元素后检查是否超过capacity是一种低效的方式。完全可以先得到集合对象的size,直接判断是否与设定的capacity冲突。
2.源码解析
入队和出队操作
队列是先进先出的结构,入队元素会被添加到队尾,出队元素是队列头部元素。分别对应于enqueue和dequeue方法。
1) enqueue方法
入队方法很简单,在队尾添加节点后将last指针指向新加入的节点对象。
private void enqueue(Node<E> node) {
last = last.next = node;
}
初始化方法中,last指针首先会知道一个item为null的节点对象上,因此在添加节点的过程中不会出现空指针异常。
2) dequeue方法
出队每次需要将队列头部元素取出,注意需要保证head指针始终指向的是item为null的节点。
private E dequeue() {
Node<E> h = head;
Node<E> first = h.next; // 此时h是初始化时创建的item为null的节点,first指向的是实际的队列头部
h.next = h; // help GC
head = first; // head指针指向队列头部节点
E x = first.item;
first.item = null; // 返回头部节点的item值,并将头部节点的item设置为null,成为新的head节点
return x;
}
新增节点操作
1) add方法
add方法在容量达到capacity时会抛出异常,
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
可见该方法底层调用的是offer方法,通过offer方法的返回值判断是否添加成功,如果添加失败则会抛出异常。
2) offer方法
offer方法不会抛出异常,而是在添加成功后返回true,反之,返回false。
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
if (count.get() == capacity) // 队列满了返回false
return false;
int c = -1; // 注意这里 c初始化值为是-1
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock; // 争取到put锁后,上锁
putLock.lock();
try {
if (count.get() < capacity) {
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal(); // 如果队列未满,添加元素后count值值赋予c变量,之后增加
}
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty(); // 如果c从-1到0说明是第一次加入元素,队列从空变为非空,唤醒put等待队列中的线程
return c >= 0;
}
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
3) put方法
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 {
// 如果队列已满则阻塞,可简单记忆为 wait not full,等待未满的过程
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal(); // 添加后如果还未满,可尝试唤醒另一个put等待队列中的对象
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
三种添加元素的方法进行如下总结,
- 添加元素的第一步是上锁,保证线程安全
- 新增数据直接添加到队尾即可
- 新增数据时,如果容量满了,则当前线程阻塞,直至队头元素被取出使得队列中存在空位。阻塞是通过锁实现的,具体原理也是等待队列,会在以后锁相关的笔记中说明
- 添加元素成功后,如果队列未满,会尝试唤醒putLock的等待线程;同时,如果队列不为空,会唤醒takeLock的等待线程。保证一旦满足put或take的条件,就能够唤起等待线程,不会浪费时间。
offer方法可以设置阻塞一定时间,具体原理与put方法相同,只是在一定时间范围内阻塞,
删除节点操作
队列的删除节点操作返回的是队列头节点的值,但是具体返回形式有两种,一种是返回值的同时删除头节点,另一种是返回值但是不删除节点。
删除数据关注两点,
- 删除原理
- 查看并删除和查看不删除在实现方式上的区别
查看并删除
1) take方法(阻塞)
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) { // 如果队列为空,则阻塞,wait not empty
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
- 先上锁
- 如果队列为空,则阻塞,直至队列非空;反之,使用dequeue方法删除队头节点,并返回队头节点的值
- 如果满足put或take的条件,会尝试唤醒等待队列中的线程
2) poll方法(非阻塞)
public E poll() {
final AtomicInteger count = this.count;
if (count.get() == 0) // 如果队列为空直接返回null
return null;
E x = null;
int c = -1;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
if (count.get() > 0) {
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
}
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
与take方法相比,不存在阻塞过程。
查看但不删除—peek方法
public E peek() {
if (count.get() == 0) // 队列为空则返回null
return null;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
Node<E> first = head.next;
if (first == null)
return null;
else
return first.item; // 获取队头节点的item值并返回
} finally {
takeLock.unlock();
}
}
该过程不涉及使用dequeue删除对头节点。
总结
LinkedBlockingQueue可以应用到多线程环境中,如消费者-生产者模型。队列本身也是很重要的数据结构。