挖掘队列源码之LinkedBlockingQueue
前言
探索LinkedBlockingQueue是基于JDK1.8,由注释可知是基于单链表的阻塞队列,至于其队列是否有界取决于其队列的容量大小,从严格意义上来说,它是有界队列,按照先进先出的顺序访问,新元素被插入到队列的尾部,从队列头部获取元素,提供在将新元素放入到饱满的队列中会导致阻塞,直到队列出现新的位置才会被唤醒继续往下操作的方法,还支持在从空队列中获取元素会导致阻塞,直到队列出现元素才会被唤醒的方法,通常情况下LinkedBlockingQueue比ArrayBlockingQueue具有更高的吞吐量,因为其内部采用了两个锁,意味着读写分离,所以效率上更快,但这同时也造成了队列的可预测性较差,接下来进入到主题吧。
数据结构
public class LinkedBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {
// 队列的容量大小
private final int capacity;
// 队列中元素的个数
private final AtomicInteger count = new AtomicInteger();
// 队列的头部元素,用于获取元素
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();
}
构造函数
/**
* 初始化
* 构建一个Integer.MAX_VALUE容量大小的队列
*/
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
/**
* 初始化
* @param capacity 容量大小
*/
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
/**
* 按顺序插入集合中的元素并初始化
* @param c 集合
*/
public LinkedBlockingQueue(Collection<? extends E> c) {
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();
}
}
简单方法
/**
* 唤醒阻塞,从空队列中获取元素导致阻塞直到新元素插入时被唤醒,所以该方法只会被put/offer调用
*/
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
/**
* 唤醒阻塞,新元素放入到饱满的队列中导致阻塞直到队列出现空位置时被唤醒
*/
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
/**
* 入队列
* 由于是按照先进先出的顺序访问,所以插入的元素放在末尾
*/
private void enqueue(Node<E> node) {
last = last.next = node;
}
/**
* 出队列
* LinkedBlockingQueue在初始化时head = new Node(null),所以在出队列时应该获取其next下一个元素
* @return 结果值
*/
private E dequeue() {
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;
}
/**
* 同时上锁,因为要遍历队列,防止其他线程同时修改其数据结构
*/
void fullyLock() {
putLock.lock();
takeLock.lock();
}
/**
* 释放锁
*/
void fullyUnlock() {
takeLock.unlock();
putLock.unlock();
}
/**
* 插入元素
* 当队列饱满的情况下会一直阻塞等待,直到被唤醒或被中断
* @param e 元素
*/
public void put(E e) throws InterruptedException {
if (e == null)
throw new NullPointerException();
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly(); // 可中断的锁
try {
while (count.get() == capacity) {
notFull.await(); // 阻塞等待空位置
}
enqueue(node);
c = count.getAndIncrement();
/**
* 通常情况下 notFull应该是在获取元素时去唤醒的,但是这里却出现了,以下是个人的理解
*
* 既然作者考虑了读写分离,那么在读或写的时候就不会出现另一种情况的锁,简单来说,在调用take方法时就不会出现putLock锁,在调用put时就不会出现takeLock锁
* 但是不行啊,这样子不就无法正常唤醒了吗,是的,所以为了减少获取锁的次数,因为在调用signal之前必须要先获取对应的锁,不然会抛异常
* 那么就设计成只获取一次或者说只出现一次获取锁,所以在take方法中就出现了这样子的代码
* if (c == capacity)
* signalNotFull();
* 也就是说只有在饱满队列情况下去获取元素时我才会唤醒,其余情况下我压根就不管了,所以总结一点就是它只会唤醒一次,那这样子可不行啊,多个线程阻塞的情况下只会有一个线程被唤醒,其他线程可怎么办呢?
* 所以就出现了如下的代码片段,在多次获取元素时count的值会改变,那么只要唤醒一次后,在put方法中就有机会执行下面的代码片段来唤醒其他的线程,所以这就解决问题了
*
* 总结:减少获取锁,实际上这也是因为读写分离所造成的代码,你看ArrayBlockingQueue就不需要
*/
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0) // 跟take方法中的代码片段效果一样,只会唤醒一次notEmpty
signalNotEmpty();
}
/**
* 插入元素
* 当队列饱满的情况下会阻塞等待指定时间,直到被唤醒或被中断或超时
* @param e 元素
* @param timeout 指定时间
* @param unit 时间单位
* @return 结果值
*/
public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException {
if (e == null) throw new NullPointerException();
long nanos = unit.toNanos(timeout);
int c = -1;
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
if (nanos <= 0)
return false;
nanos = notFull.awaitNanos(nanos);
}
enqueue(new Node<E>(e));
c = count.getAndIncrement();
if (c + 1 < capacity) // 可参考put方法中的解释
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return true;
}
/**
* 插入元素
* 当队列饱满的情况下会返回false
* @param e 元素
* @return 结果值
*/
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;
}
/**
* 获取元素
* 当队列为空的情况下一直阻塞等待,直到被唤醒或被中断
* @return 结果值
*/
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) // 与put方法中的作用有异曲同工
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
/**
* 获取元素
* 当队列为空的情况下阻塞等待指定时间,直到被唤醒或被中断或超时
* @param timeout 指定时间
* @param unit 时间单位
* @return 结果值
*/
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
E x = null;
int c = -1;
long nanos = unit.toNanos(timeout);
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
if (nanos <= 0)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
/**
* 获取元素
* 当队列为空的情况下直接返回null
* @return 结果值
*/
public E poll() {
final AtomicInteger count = this.count;
if (count.get() == 0)
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;
}
/**
* 获取元素
* 与前面的三个方法较大不同,因为其不会导致head指向发生改变,所以该方法即使调用多次依然能获取到元素,而其他三个方法则不行
* @return 结果值
*/
public E peek() {
if (count.get() == 0)
return null;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
Node<E> first = head.next;
if (first == null)
return null;
else
return first.item;
} finally {
takeLock.unlock();
}
}
/**
* 移除指定节点,解除关联关系
* @param p 指定节点
* @param trail 上一个节点
*/
void unlink(Node<E> p, Node<E> trail) {
p.item = null;
trail.next = p.next;
if (last == p)
last = trail;
if (count.getAndDecrement() == capacity)
notFull.signal();
}
/**
* 移除指定元素
* @param o 指定元素
* @return 是否移除成功
*/
public boolean remove(Object o) {
if (o == null) return false;
fullyLock(); // 由于后续要遍历队列,所以应该防止其他线程修改其数据结构,否则将造成数据不一致
try {
for (Node<E> trail = head, p = trail.next;
p != null;
trail = p, p = p.next) {
if (o.equals(p.item)) {
unlink(p, trail);
return true;
}
}
return false;
} finally {
fullyUnlock();
}
}
总结
LinkedBlockingQueue提供了多种插入/获取元素的方法,先简单说下区别:
-
在队列饱满的情况下
插入新元素-
add:直接抛出异常。
-
offer:直接返回false。
-
put:一直阻塞等待直到被唤醒或线程被中断。
-
offer(time):阻塞等待指定时间,直到被唤醒或被中断或
超时。
-
-
在队列为空的情况下
获取元素-
poll:直接返回null。
-
take:一直被阻塞等待直到被唤醒或线程被中断。
-
poll(time):阻塞等待直到被唤醒或线程被中断或
超时。 -
peek:直接返回null,该方法即使调用多次依然能获取到元素,而其余3个方法则不行。
-
与ArrayBlockingQueue完全一致...说下不同点吧,为了方便ArrayBlockingQueue将直接写成ABQ,LinkedBlockingQueue写成LBQ。
-
ABQ同一时间
要么是读要么写,LBQ读写分离,所以LBQ的吞吐量比ABQ高,但预测性较差。 -
ABQ数据结构是
数组有界阻塞队列,LBQ数据结构是单链表有界阻塞队列。
关于LBQ读写分离的代码与ABQ有着较大的区别,读者最好去理解下!
浙公网安备 33010602011771号