概述
LinkedBlockingQueue用链表实现的有界阻塞队列,初始化时如果没有传入容量,则容量时Intger.MAX_VALUE,是FIFO队列,因为入队和出队方法各是一把锁,所以一般情况下并发性能优于ArrayBlockingQueue。本篇一起来看源码,LinkedBlockingQueue的并发控制是用ReentrantLock加Condition。看本篇前可以先看我写的另外三篇文章,ReentrantLock源码分析, Condition源码分析,阻塞队列简介。
类结构
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
//节点数据结构,可以看到是单向列表
static class Node<E> {
E item;
//指向下一节点
Node<E> next;
Node(E x) { item = x; }
}
//容量,即队列的最大元素数,说明该队列是有界的
private final int capacity;
//当前队列的元素数量,用AtomicInteger保证并发的原子性
private final AtomicInteger count = new AtomicInteger();
//队列的头节点,出队从这里
transient Node<E> head;
//队列的尾节点,入队从这里,可以看到,队列是FIFO的
private transient Node<E> last;
//出队的锁控制
private final ReentrantLock takeLock = new ReentrantLock();
//队列为空时,出队take方法的线程进入等待
private final Condition notEmpty = takeLock.newCondition();
//入队的锁控制
private final ReentrantLock putLock = new ReentrantLock();
//入队时队列已满,将入队线程进入等待
private final Condition notFull = putLock.newCondition();
。。。
}
上面给出了LinkedBlockingQueue主体结构,每个节点存放目标元素并指向下一节点,并发的控制使用ReentrantLock,需要等待用Condition。在阻塞队列简介中,我们说过入队方法等待,只有put和带超时时间的offer方法。所以我们后面可以看到只有这两个方法会用到notFull.await。
另外我们学习ReentrantLock时知道,它里面也有队列(它的队列节点是线程,获取锁未成功的线程进入队列等候,调用condition.await,就进入condition队列),请不要和这里的混淆,这里是目标元素的队列。
构造方法
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
//初始化容量
this.capacity = capacity;
//初始化头尾的指针,可以看到队列的头尾指针不可能为null
last = head = new Node<E>(null);
}
put方法
public void put(E e) throws InterruptedException {
//不允许空元素
if (e == null) throw new NullPointerException();
// 队列的节点数量,初始化负数,如果后面还是负数表示入队失败offer方法会用到
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
//入队前先加锁,将后面的并发操作变成单线程操作
putLock.lockInterruptibly();
try {
//队列已满时,线程进入等待队列
//被唤醒后会再次判断,是否已满,所以进行while循环
while (count.get() == capacity) {
notFull.await();
}
//入队,到了这里肯定是线程没满,且只有一个线程操作,所以直接入队
enqueue(node);
//拿到入队前的队列节点数量,并原子性加1,这里为什么要使用AtomicIntege呢?
//虽然这里只会有一个线程运行,但还有出队takeLock,使用另一把锁,也在对count操作,产生竞争
c = count.getAndIncrement();
//如果队列没满唤起notFull等待的线程
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
//如果队列是空,唤起notEmpty等待的线程
if (c == 0)
signalNotEmpty();
}
enqueue方法
private void enqueue(Node<E> node) {
// 前面代码上了锁,这里是单线程操作,可以看到代码很简单,就是入队尾
last = last.next = node;
}
signalNotEmpty方法
//唤醒notEmpty的Condition条件等待
//这个等待是出队方法take或超时poll,出队时,队列为空,线程进行的等待
private void signalNotEmpty() {
//为什么这里加锁呢?
//因为每个出队方法(take、poll)出队后,队列不为空都会进行唤醒,后面代码会看到
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
put方法就介绍完了,offer方法很类似,我们简单看下
offer方法
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
if (count.get() < capacity) {
//入队,上面已经分析
enqueue(node);
c = count.getAndIncrement();
//队列没满,唤醒入队的notFull条件等待线程
//为什么是c+1?因为使用的是count.getAndIncrement返回的值是加1前的值,即入队前的节点个数
//所以c+1才是当前队列的节点数
if (c + 1 < capacity)
notFull.signal();
}
} finally {
putLock.unlock();
}
//队列为空,进行加锁唤醒notEmpty条件等待线程
if (c == 0)
signalNotEmpty();
return c >= 0;
}
take 方法
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
//加take读写锁
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
//当count为0时,当前线程进行条件等待
while (count.get() == 0) {
notEmpty.await();
}
//被唤醒后,队列不为空,进行出队操作
x = dequeue();
//count值原子性减1
c = count.getAndDecrement();
//如果队列不为空,对notEmpty条件等待的线程唤醒,所以put方法的notEmpty.signal要先加takeLock锁
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
//take前队列时满的,可能有线程入队进行了等待,所以唤醒notFull条件等待线程
if (c == capacity)
signalNotFull();
return x;
}
private E dequeue() {
//我们从上面的构造方法中看到,初始化时队列的头尾指针都指向了一个item为null的空节点
//所以这里出队就是把head.next节点的item返回,把头节点后移一个就行了
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;
}
唤醒notFull条件等待线程
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
take方法就分析完啦,poll方法和take方法类似,比较简单就不分析了,我们来看下remove方法。
remove方法
public boolean remove(Object o) {
if (o == null) return false;
//将putLock,takeLock全部锁定,为什么?
//因为remove object可能操作的是头节点,也可能操作的是尾节点,都加锁才能保证头尾节点的操作不会因竞争产生错误。
fullyLock();
try {
for (Node<E> trail = head, p = trail.next;
p != null;
trail = p, p = p.next) {
//如果节点中的对象和remove的目标对象相等,进行移除
if (o.equals(p.item)) {
//移除节点
unlink(p, trail);
return true;
}
}
//没找到节点返回false
return false;
} finally {
fullyUnlock();
}
}
fullyLock方法
void fullyLock() {
putLock.lock();
takeLock.lock();
}
unlink 方法
void unlink(Node<E> p, Node<E> trail) {
// p是待移除节点,trail是p的前继节点
p.item = null;
//将p的后继节点赋给trail的next
//p的item设为null,完成了remove操作
trail.next = p.next;
//如果p是尾节点,需要将trail赋值给last引用
if (last == p)
last = trail;
//如果移除之前队列是满的,可能有入队线程在等待入队,进行唤醒操作
if (count.getAndDecrement() == capacity)
notFull.signal();
}