简介
LinkedBlockingQueue是一个用链表实现的有界阻塞队列,此队列按照先进先出(FIFO)的原则对元素进行排序。
此队列的默认和最大长度是Integer.MAX_VALUE,LinkedBlockingQueue类有三个构造方法:
// 默认构造方法,该方法会调用this(Integer.MAX_VALUE),即默认最大长度是Integer.MAX_VALUE public LinkedBlockingQueue(); // 参数capacity为指定的队列最大长度 public LinkedBlockingQueue(int capacity); // 根据Collection来创建队列,会将集合中的元素加入到队列中 public LinkedBlockingQueue(Collection<? extends E> c);
LinkedBlockingQueue源码详解
LinkedBlockingQueue类定义为:
public class LinkedBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable
该类同样继承了AbstractQueue抽象类并实现了BlockingQueue接口,这里不再叙述。
LinkedBlockingQueue类中的数据都被封装成了Node对象:
static class Node<E> { // 节点数据 E item; // 下一节点 Node<E> next; Node(E x) { item = x; } }
同时,LinkedBlockingQueue类通过ReentrantLock和Condition来确保多线程环境下的同步问题。
/** The capacity bound, or Integer.MAX_VALUE if none */ private final int capacity; /** Current number of elements */ private final AtomicInteger count = new AtomicInteger(); /** * Head of linked list. * Invariant: head.item == null */ transient Node<E> head; /** * Tail of linked list. * Invariant: last.next == null */ private transient Node<E> last; /** Lock held by take, poll, etc */ private final ReentrantLock takeLock = new ReentrantLock(); /** Wait queue for waiting takes */ private final Condition notEmpty = takeLock.newCondition(); /** Lock held by put, offer, etc */ private final ReentrantLock putLock = new ReentrantLock(); /** Wait queue for waiting puts */ private final Condition notFull = putLock.newCondition();
- capacity:阻塞队列的容量,默认值为Integer.MAX_VALUE,可通过构造函数设置
- count:阻塞队列中的元素个数
- head:阻塞队列头结点
- last:阻塞队列尾结点
- takeLock:获取元素时都必须获取该锁
- notEmpty:出队条件
- putLock:添加元素时都必须获取该锁
- notFull:入队条件
LinkedBlockingQueue类有两个独占锁:takeLock和putLock,也就是说,添加和删除元素并不是互斥操作,可以同时进行,这样也就可以提高吞吐量。
入队
我们来看一下add(E e)方法:
LinkedBlockingQueue类的add(E e)方法继承自AbstractQueue:
public boolean add(E e) { if (offer(e)) return true; else throw new IllegalStateException("Queue full"); }
LinkedBlockingQueue类实现的offer(E e)方法为:
public boolean offer(E e) { // 若插入的数据为null,则抛出NullPointerException异常 if (e == null) throw new NullPointerException(); // 获取队列中的元素个数 final AtomicInteger count = this.count; // 若队列已满则返回false if (count.get() == capacity) return false; int c = -1; // 将对象构建为Node节点 Node<E> node = new Node<E>(e); // 获取putLock独占锁 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独占锁 putLock.unlock(); } // 队列中还有一条数据(c的初始值为-1),唤醒阻塞在非空条件上的线程 if (c == 0) signalNotEmpty(); return c >= 0; }
LinkedBlockingQueue类的offer(E e)方法与ArrayBlockingQueue类相比,主要有两处不同:
1、ArrayBlockingQueue类在添加元素之后,会唤醒消费线程,而LinkedBlockingQueue类在添加完元素之后,若队列未满,它会唤醒其他的生产进程,产生该差异的主要原因是ArrayBlockingQueue使用了一个独占锁,而LinkedBlockingQueue使用了两个独占锁,这两个锁可以分别控制元素入队与出队操作,而不会产生同步问题,可以提高吞吐量。
2、LinkedBlockingQueue类在添加完元素之后,也可能会唤醒消费线程,唤醒的前提是c==0,那为什么是这样呢?这也与两个独占锁有关,因为消费线程在消费数据时,只会获取takeLock,所以说,生产线程生产数据时,不会影响消费线程,若队列中的元素一直>0,消费线程是不会停止的,可能没有消费线程停止 。而在c=0的时,这表明,可能有消费线程已经停止,这时,就需要唤醒消费线程,来取出队列中的数据。
出队
我们这边来看一下poll()方法:
public E poll() { // 获取队列的元素个数 final AtomicInteger count = this.count; // 队列为空,则直接返回null if (count.get() == 0) return null; E x = null; int c = -1; // 获取takeLock独占锁 final ReentrantLock takeLock = this.takeLock; takeLock.lock(); try { // 若队列非空 if (count.get() > 0) { // 元素出队 x = dequeue(); // 更新元素个数值 c = count.getAndDecrement(); // 如果队列非空,则唤醒阻塞在非空条件上的线程 if (c > 1) notEmpty.signal(); } } finally { // 释放takeLock独占锁 takeLock.unlock(); } // 若队列已满,则唤醒阻塞在非满条件上的线程 if (c == capacity) signalNotFull(); return x; }
该类的poll()方法与ArrayBlockingQueue类相比,仍然有两处不同,具体原因不再解释,类似于offer(E e)方法。
因为LinkedBlockingQueue类具有两个独占锁,该类的其他方法与ArrayBlockingQueue相比,也有一些不同,比如remove(Object o)方法:
public boolean remove(Object o) { // 若要删除的元素为null,则直接返回false if (o == null) return false; // 获取两个独占锁 fullyLock(); try { // 遍历查找要删除的节点,将其删除,成功返回true,否则,返回false 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(); } }
我们看到该方法调用了一个fullyLock()方法
void fullyLock() { putLock.lock(); takeLock.lock(); }
该方法是获取两个独占锁,为什么要或者这两个锁呢?因为remove(Object o)方法删除的元素的位置不确定,可能会产生同步问题,所以要获取两个锁。
相关博客
Java并发编程之ArrayBlockingQueue阻塞队列详解
参考资料
方腾飞:《Java并发编程的艺术》