BlockingQueue
在分析BlockingQueue之前,先看一下JDK1.8中对于BlockingQueue的注释说明:
A {@link java.util.Queue} that additionally supports operations that wait for the queue to become non-empty when retrieving an element, and wait for space to become available in the queue when storing an element.
即获取元素的时候等待队列变为非空,以及存储元素的时候等待队列变为可用。
BlockingQueue有这几个特点:不接受null元素、可能是容量有限的、主要用作生产者-消费者队列、线程安全等。
BlockingQueue同时提供阻塞和非阻塞的方法,例如:
boolean offer(E e); boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;
前者立即返回插入结果,后者可以在超时时间之内等待队列可用的时候再执行插入操作。
在JDK1.8中提供了BlockingQueue的七种实现以及一种ScheduledThreadPoolExecutor的内部实现:
ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue :一个由链表结构组成的无界阻塞队列。
PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
接下来将一个个分析每一种队列。
ArrayBlockingQueue
ArrayBlockingQueue是基于数组的队列,构造函数初始化的时候生成定长数组,基于FIFO的原则对数组元素进行操作。
ArrayBlockingQueue内部的阻塞队列是通过重入锁ReenterLock和Condition条件队列实现的,所以ArrayBlockingQueue中的元素存在公平访问与非公平访问的区别,对于公平访问队列,被阻塞的线程可以按照阻塞的先后顺序访问队列,即先阻塞的线程先访问队列。而非公平队列,当队列可用时,阻塞的线程将进入争夺访问资源的竞争中,也就是说谁先抢到谁就执行,没有固定的先后顺序。
public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable { private static final long serialVersionUID = -817911632652898426L; /** The queued items */ final Object[] items; /** items index for next take, poll, peek or remove */ int takeIndex; /** items index for next put, offer, or add */ int putIndex; /** Number of elements in the queue */ int count; /** Main lock guarding all access 锁,读写共用一个锁*/ final ReentrantLock lock; /** Condition for waiting takes 出列条件 */ private final Condition notEmpty; /** Condition for waiting puts 入列条件 */ private final Condition notFull; /** * Shared state for currently active iterators, or null if there * are known not to be any. Allows queue operations to update * iterator state. */ transient Itrs itrs = null; }
ArrayBlockingQueue的入列操作:
add(E e) :将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量),在成功时返回 true,如果此队列已满,则抛出 IllegalStateException
offer(E e) :将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量),在成功时返回 true,如果此队列已满,则返回 false
offer(E e, long timeout, TimeUnit unit) :将指定的元素插入此队列的尾部,如果该队列已满,则在到达指定的等待时间之前等待可用的空间,超时返回 false
put(E e) :将指定的元素插入此队列的尾部,如果该队列已满,则等待可用的空间
四个方法都是通过调用enqueue(E e)方法将元素插入到数组的:
private void enqueue(E x) { // assert lock.getHoldCount() == 1; // assert items[putIndex] == null; final Object[] items = this.items; items[putIndex] = x; if (++putIndex == items.length) putIndex = 0; count++; notEmpty.signal(); }
enqueue方法就是在putIndex位置添加元素,如果putIndex+1位于队尾则移动到队伍头部,并且通知阻塞在等待出列状态的线程。
ArrayBlockingQueue的出列操作:
poll() :获取并移除此队列的头,如果此队列为空,则返回 null
poll(long timeout, TimeUnit unit) :获取并移除此队列的头部,在指定的等待时间前等待可用的元素(如果有必要)
take() :获取并移除此队列的头部,在元素变得可用之前一直等待(如果有必要)
remove(Object o) :从此队列中移除指定元素的单个实例(如果存在)
前三个方法使用dequeue()取出队列头部元素:
private E dequeue() { // assert lock.getHoldCount() == 1; // assert items[takeIndex] != null; final Object[] items = this.items; @SuppressWarnings("unchecked") E x = (E) items[takeIndex]; items[takeIndex] = null; if (++takeIndex == items.length) takeIndex = 0; count--; if (itrs != null) itrs.elementDequeued(); notFull.signal(); return x; }
dequeue方法就是在takeIndex的位置取出元素(元素置空),takeIndex的位置后移一位,同时维护迭代期 itrs 对象的信息,最后通知阻塞在等待入列状态的线程。
remove方法,先遍历找到元素,如果找到则调用私有方法removeAt(final int removeIndex),方法从removeIndex到putIndex,一个个前移元素,最后通知阻塞在等待入列的线程。
LinkedBlockingQueue
LinkedBlockingQueue是一个由链表实现的有界队列阻塞队列,但大小默认值为Integer.MAX_VALUE,所以我们在使用LinkedBlockingQueue时建议手动传值,为其提供我们所需的大小,避免队列过大造成机器负载或者内存爆满等情况。其构造函数如下:
//默认大小为Integer.MAX_VALUE public LinkedBlockingQueue() { this(Integer.MAX_VALUE); } //创建指定大小为capacity的阻塞队列 public LinkedBlockingQueue(int capacity) { if (capacity <= 0) throw new IllegalArgumentException(); this.capacity = capacity; last = head = new Node<E>(null); } //创建大小默认值为Integer.MAX_VALUE的阻塞队列并添加c中的元素到阻塞队列 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(new Node<E>(e)); ++n; } count.set(n); } finally { putLock.unlock(); } }
从源码看,有三种方式可以构造LinkedBlockingQueue,通常情况下,我们建议创建指定大小的LinkedBlockingQueue阻塞队列。
LinkedBlockingQueue队列也是按 FIFO(先进先出)排序元素。队列的头部是在队列中时间最长的元素,队列的尾部 是在队列中时间最短的元素,新元素插入到队列的尾部,而队列执行获取操作会获得位于队列头部的元素。在正常情况下,链接队列的吞吐量要高于基于数组的队列(ArrayBlockingQueue),因为其内部实现添加和删除操作使用的两个ReentrantLock来控制并发执行,而ArrayBlockingQueue内部只是使用一个ReentrantLock控制并发,因此LinkedBlockingQueue的吞吐量要高于ArrayBlockingQueue。
public class LinkedBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable { /** * 节点类,用于存储数据 */ static class Node<E> { E item; /** * One of: * - the real successor Node * - this Node, meaning the successor is head.next * - null, meaning there is no successor (this is the last node) */ Node<E> next; Node(E x) { item = x; } } /** 阻塞队列的大小,默认为Integer.MAX_VALUE */ private final int capacity; /** 当前阻塞队列中的元素个数 */ private final AtomicInteger count = new AtomicInteger(); /** 阻塞队列的头结点 */ transient Node<E> head; /** 阻塞队列的尾节点 */ private transient Node<E> last; /** 获取并移除元素时使用的锁,如take, poll, etc */ private final ReentrantLock takeLock = new ReentrantLock(); /** notEmpty条件对象,当队列没有数据时用于挂起执行删除的线程 */ private final Condition notEmpty = takeLock.newCondition(); /** 添加元素时使用的锁如 put, offer, etc */ private final ReentrantLock putLock = new ReentrantLock(); /** notFull条件对象,当队列数据已满时用于挂起执行添加的线程 */ private final Condition notFull = putLock.newCondition(); }
LinkedBlockingQueue的add添加方法是基于offer(E e)实现的,offer(E e)的执行源码如下:
public boolean offer(E e) { //添加元素为null直接抛出异常 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(); } // 由于存在添加锁和消费锁,而消费锁和添加锁都会持续唤醒等到线程,因此count肯定会变化。 //这里的if条件表示如果队列中还有1条数据 if (c == 0) signalNotEmpty();//如果还存在数据那么就唤醒消费锁 return c >= 0; // 添加成功返回true,否则返回false } //入队操作 private void enqueue(Node<E> node) { //队列尾节点指向新的node节点 last = last.next = node; } //signalNotEmpty方法 private void signalNotEmpty() { final ReentrantLock takeLock = this.takeLock; takeLock.lock(); //唤醒获取并删除元素的线程 notEmpty.signal(); } finally { takeLock.unlock(); } }
这里的offer()方法做了两件事,第一件事是判断队列是否满,满了就直接释放锁,没满就将节点封装成Node入队,然后再次判断队列添加完成后是否已满,不满就继续唤醒等到在条件对象notFull上的添加线程。第二件事是,判断是否需要唤醒等到在notEmpty条件对象上的消费线程。
唤醒添加线程的原因,在添加新元素完成后,会判断队列是否已满,不满就继续唤醒在条件对象notFull上的添加线程,这点与前面分析的ArrayBlockingQueue很不相同,在ArrayBlockingQueue内部完成添加操作后,会直接唤醒消费线程对元素进行获取,这是因为ArrayBlockingQueue只用了一个ReenterLock同时对添加线程和消费线程进行控制,这样如果在添加完成后再次唤醒添加线程的话,消费线程可能永远无法执行,而对于LinkedBlockingQueue来说就不一样了,其内部对添加线程和消费线程分别使用了各自的ReenterLock锁对并发进行控制,也就是说添加线程和消费线程是不会互斥的,所以添加锁只要管好自己的添加线程即可,添加线程自己直接唤醒自己的其他添加线程,如果没有等待的添加线程,直接结束了。如果有就直到队列元素已满才结束挂起,当然offer方法并不会挂起,而是直接结束,只有put方法才会当队列满时才执行挂起操作。注意消费线程的执行过程也是如此。这也是为什么LinkedBlockingQueue的吞吐量要相对大些的原因。
判断if (c == 0)
时才去唤醒消费线程的原因,消费线程一旦被唤醒是一直在消费的(前提是有数据),所以c值是一直在变化的,c值是添加完元素前队列的大小,此时c只可能是0或c>0
,如果是c=0
,那么说明之前消费线程已停止,条件对象上可能存在等待的消费线程,添加完数据后应该是c+1
,那么有数据就直接唤醒等待消费线程,如果没有就结束啦,等待下一次的消费操作。如果c>0
那么消费线程就不会被唤醒,只能等待下一个消费操作(poll、take、remove)的调用,那为什么不是条件c>0
才去唤醒呢?我们要明白的是消费线程一旦被唤醒会和添加线程一样,一直不断唤醒其他消费线程,如果添加前c>0
,那么很可能上一次调用的消费线程后,数据并没有被消费完,条件队列上也就不存在等待的消费线程了,所以c>0
唤醒消费线程得意义不是很大,当然如果添加线程一直添加元素,那么一直c>0
,消费线程执行的换就要等待下一次调用消费操作了(poll、take、remove)。
LinkedBlockingQueue的移除方法主要有remove、poll和take。
remove会对读锁和写锁同时上锁,因为remove方法删除的数据的位置不确定,为了避免造成并发安全问题,所以需要对两个锁同时上锁。remove执行过程中会对链表进行遍历,找到元素然后删除,最后解锁。
poll方法也比较简单,如果队列没有数据就返回null,如果队列有数据,那么就取出来,如果队列还有数据那么唤醒等待在条件对象notEmpty上的消费线程。然后判断if (c == capacity)为true就唤醒添加线程,这点与前面分析if(c==0)是一样的道理。因为只有可能队列满了,notFull条件对象上才可能存在等待的添加线程。
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(); //如果队列未空,继续唤醒等待在条件对象notEmpty上的消费线程 if (c > 1) notEmpty.signal(); } } finally { takeLock.unlock(); } //判断c是否等于capacity,这是因为如果满说明NotFull条件对象上 //可能存在等待的添加线程 if (c == capacity) signalNotFull(); return x; } private E dequeue() { Node<E> h = head;//获取头结点 Node<E> first = h.next; 获取头结的下一个节点(要删除的节点) h.next = h; // help GC//自己next指向自己,即被删除 head = first;//更新头结点 E x = first.item;//获取删除节点的值 first.item = null;//清空数据,因为first变成头结点是不能带数据的,这样也就删除队列的带数据的第一个节点 return x; }
take方法的执行方式和poll类似,但是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();//队列大小减1 if (c > 1) notEmpty.signal();//还有数据就唤醒后续的消费线程 } finally { takeLock.unlock(); } //满足条件,唤醒条件对象上等待队列中的添加线程 if (c == capacity) signalNotFull(); return x; }
LinkedBlockingQueue和ArrayBlockingQueue的区别
1.队列大小有所不同,ArrayBlockingQueue是有界的初始化必须指定大小,而LinkedBlockingQueue可以是有界的也可以是无界的(Integer.MAX_VALUE),对于后者而言,当添加速度大于移除速度时,在无界的情况下,可能会造成内存溢出等问题。
2.数据存储容器不同,ArrayBlockingQueue采用的是数组作为数据存储容器,而LinkedBlockingQueue采用的则是以Node节点作为连接对象的链表。
3.由于ArrayBlockingQueue采用的是数组的存储容器,因此在插入或删除元素时不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响。
4.两者的实现队列添加或移除的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁,而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
参考:https://blog.csdn.net/javazejian/article/details/77410889 深入剖析java并发之阻塞队列LinkedBlockingQueue与ArrayBlockingQueue