阻塞队列LinkedBlockingQueue 与平常接触的LinkedList 的最大不同之处就是LinkedBlockingQueue 支持阻塞添加与阻塞删除方法。
阻塞添加
阻塞添加是指当队列满时,阻塞加入线程,直到不满时才唤醒阻塞线程,才能执行加入操作。
阻塞删除
阻塞删除是指当队列为空时,阻塞删除线程,直到不为空才唤醒阻塞线程,才能执行删除操作。
由此可见这不就是生产者消费者模型吗?在生产者消费者模型的理解上,再来看阻塞队列,生产操作就是阻塞添加,消费操作就是阻塞删除。
阻塞队列接口BlockingQueue继承自Queue接口,看看相关方法,
public interface BlockingQueue<E> extends Queue<E> {
// 将元素插入到队列尾部,如果还没有达到队列容量成功返回true ,
// 队列已满,则抛出IllegalStateException
boolean add(E e);
// 将元素插入到队列尾部,成功返回true ,如果此队列已满,则返回false
boolean offer(E e);
// 将元素插入到队列尾部,如果队列已满,则可以等待指定时间,
// 若在时间内,有空间了,则可以插入成功,返回true ,
// 否则返回false
boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException;
// 将元素插入到队列尾部,队列没有达到容量则成功,
// 如果队列已满,则阻塞,直到被唤醒
void put(E e) throws InterruptedException;
// 获取并移除队列头部元素,队列有元素,则成功,
// 如果没有元素,则阻塞,直到被唤醒
E take() throws InterruptedException;
// 获取并移除队列头部元素,队列有元素,则成功,
// 若队列没有元素,则可等待指定时间来完成操作
E poll(long timeout, TimeUnit unit)
throws InterruptedException;
// 从队列移除指定元素
boolean remove(Object o);
// ...
}
除了上述方法还有继承自Queue接口的方法,
// 获取但不移除队列的头部元素,没有则抛出NoSuchElementException
E element();
// 获取但不移除队列的头部元素,如果此队列为空,则返回 null
E peek();
// 获取并移除队列的头部元素,如果此队列为空,则返回 null
E poll();
这里把上述操作进行分类,
插入方法,
add(E e) : 添加成功返回true,失败抛异常
offer(E e) : 成功返回 true,如果此队列已满,则返回 false
put(E e) :将元素插入此队列的尾部,如果该队列已满,则阻塞,直到被唤醒
删除方法,
remove(Object o) :移除指定元素,成功返回true,失败返回false
poll() : 获取并移除此队列的头部元素,若队列为空,则返回 null
take():获取并移除此队列头部元素,若没有元素则一直阻塞,则阻塞,直到被唤醒
检查方法,
element() :获取但不移除此队列的头部元素,没有元素则抛异常
peek() :获取但不移除此队列的头头部元素,若队列为空,则返回 null
通常情况下通过这3类方法(增删查)操作阻塞队列,接下来看看LinkedBlockingQueue 的原理。
LinkedBlockingQueue 原理
LinkedBlockingQueue 是一个基于链表的阻塞队列,首先看一下它的属性字段,
// 元素通过Node这个静态内部类进行存储,这与LinkedList的处理方式一样
static class Node<E> {
// 使用item来保存元素本身
E item;
// 当前节点的后继节点
Node<E> next;
Node(E x) { item = x; }
}
// 阻塞队列所能存储的最大容量
private final int capacity;
// 当前阻塞队列的元素数量
private final AtomicInteger count = new AtomicInteger(0);
// 链表的头部,注意head.item==null
private transient Node<E> head;
// 链表的尾部,注意last.next==null
private transient Node<E> last;
// 控制出队列的锁
private final ReentrantLock takeLock = new ReentrantLock();
// 控制出队列的Condition
private final Condition notEmpty = takeLock.newCondition();
// 控制入队列的锁
private final ReentrantLock putLock = new ReentrantLock();
// 控制入队列的Condition
private final Condition notFull = putLock.newCondition();
可见LinkedBlockingQueue 的入队与出队使用的是不同的锁,所以入队与出队之间不会互斥,这显然可以提高效率,所以计数count 变量用的原子操作类,因为入队与出队都会对该变量就行更改。
看下LinkedBlockingQueue 的构造方法,
// 如果用户没有显示指定capacity的值,默认使用Integer.MAX_VALUE
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
// 可见,当队列没有任何元素时,此时队列的头与尾都指向同一个节点,且元素内容为null
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
// 初始化在初始化LinkedBlockingQueue ,也可以将一个集合作为参数,
// 将该集合元素入队。LinkedBlockingQueue 最大容量是Integer.MAX_VALUE
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();
if (n == capacity)
throw new IllegalStateException("Queue full");
//参考下面的enqueue分析
enqueue(new Node<E>(e));
++n;
}
count.set(n);
} finally {
//释放锁
putLock.unlock();
}
}
// 入队操作
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node;
}
这里说一下入队方法enqueue() ,可以看到该方法虽写法简单,但代码的可读性并不清晰,其实就是这样的逻辑,
private void enqueue(Node<E> node) {
last.next = node;
last = node;
}
接下来看一下put 方法,
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// 注意:约定所有的put/take操作都会预先设置本地变量
int c = -1;
Node<E> node = new Node(e);
// 将putLock 赋值给了一个局部变量
final ReentrantLock putLock = this.putLock;
// 计数也赋值了局部变量
final AtomicInteger count = this.count;
// 可中断的锁获取操作
// 这里如果执行线程设置了中断,那么调用该方法获取锁会响应该中断,抛异常
putLock.lockInterruptibly();
try {
// 当队列满了,此时当前线程将进入等待状态,直到有空间被唤醒才继续执行
while (count.get() == capacity) {
notFull.await();
}
// 让元素入队
enqueue(node);
// 获取队列元素个数,然后再+1 (由于生产了一个,所以+1)
// 注意,这里拿到的是队列原来的元素个数
c = count.getAndIncrement();
// 当前元素的个数为 c + 1 ,就是现在的个数 + 1 ,
// 当前队列元素个数 < 容量,所以唤醒其他入队线程,使生产更快
if (c + 1 < capacity)
notFull.signal();
} finally {
// 释放锁
putLock.unlock();
}
// 原来为队列空,所以可能有出队线程处于等待状态,
// 而又生产了一个,所以队列不再为空了,执行唤醒出队线程的操作
if (c == 0)
signalNotEmpty();
}
// 唤醒出队线程操作
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
//执行唤醒获取元素线程的操作
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
可以看到就是生产者模型,在队列满了等待,生产成功了,唤醒消费者,生产再则是将元素包装成节点,插入到队列尾部。
看一下offer 方法,因为该方法在线程池用到了,所以简单说一下,其实该方法与put 方法差不多,只不过在队列满了的情况不是阻塞,而是直接返回false ,
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;
}
接下来看看LinkedBlockingQueue 的出队过程,看一下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) {
notEmpty.await();
}
// 出队操作,获取并移除队列头部元素
x = dequeue();
c = count.getAndDecrement();
// c 为原来元素个数,那么消费了一个现在就是c -1 个
// c > 1 => c - 1 > 0 ,现在的个数 > 0 ,所以要唤醒消费者,使消费的更快
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
// c == capacity 说明之前是满的,
// 现在消费了一个,自然就不是满的了,所以要唤醒生产者去生产
if (c == capacity)
signalNotFull();
return x;
}
// 从队列头部移除一个节点
private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
Node<E> h = head;
Node<E> first = h.next; // 这是第一个元素
h.next = h; // 帮助 GC
// head 指向这个要移除的第一个元素
head = first;
// 取出元素
E x = first.item;
// 将元素置为空,因为该节点要作为新的头节点了
first.item = null;
return x;
}
取出元素是将原头结点断链,下一个节点取出元素返回,并将该节点置空来作为新的头结点。