阻塞队列介绍

Queue接口

  1. public interface Queue<E> extends Collection<E> {
  2. //添加一个元素,添加成功返回true, 如果队列满了,就会抛出异常
  3. boolean add(E e);
  4. //添加一个元素,添加成功返回true, 如果队列满了,返回false
  5. boolean offer(E e);
  6. //返回并删除队首元素,队列为空则抛出异常
  7. E remove();
  8. //返回并删除队首元素,队列为空则返回null
  9. E poll();
  10. //返回队首元素,但不移除,队列为空则抛出异常
  11. E element();
  12. //获取队首元素,但不移除,队列为空则返回null
  13. E peek();
  14. }

BlockingQueue接口

BlockingQueue 继承了 Queue 接口,是队列的一种。Queue 和 BlockingQueue 都是在 Java 5 中加入的。阻塞队列(BlockingQueue)是一个在队列基础上又支持了两个附加操作的队列,常用解耦。两个附加操作:

  • put:支持阻塞的插入方式,队列满时,队列会阻塞插入元素的线程,直到队列不满
  • take:支持阻塞的移除方法:队列为空时,获取元素的线程会等待队列变为非空 ```java public interface BlockingQueue extends Queue {

    boolean add(E e); boolean offer(E e); void put(E e) throws InterruptedException; boolean offer(E e, long timeout, TimeUnit unit)

    1. throws InterruptedException;

}

  1. BlockingQueueJDK集合包中的Queue接口兼容,同时在其基础上增加了阻塞功能。<br />(1offer(E e):如果队列没满,返回true,如果队列已满,返回false(不阻塞)<br />(2offer(E e, long timeout, TimeUnit unit):可以设置阻塞时间,如果队列已满,则进行阻塞。超过阻塞时间,则返回false<br />(3put(E e):队列没满的时候是正常的插入,如果队列已满,则阻塞,直至队列空出位置 <br />**出队:**<br />(1poll():如果有数据,出队,如果没有数据,返回null (不阻塞)<br />(2poll(long timeout, TimeUnit unit):可以设置阻塞时间,如果没有数据,则阻塞,超过阻塞时间,则返回null<br />(3take():队列里有数据会正常取出数据并删除;但是如果队列里无数据,则阻塞,直到队列里有数据
  2. **BlockingQueue常用方法示例**<br />当队列满了无法添加元素,或者是队列空了无法移除元素时:
  3. | 方法 | 抛出异常 | 返回特定值 | 阻塞 | 阻塞特定时间 |
  4. | --- | --- | --- | --- | --- |
  5. | 入队 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
  6. | 出队 | remove() | poll() | take() | poll(time, unit) |
  7. | 获取队首元素 | element() | peek() | 不支持 | 不支持 |
  8. <a name="CGk1y"></a>
  9. #### 阻塞队列特性
  10. **阻塞**<br />**阻塞功能使得生产者和消费者两端能力得到平衡,**<br />**最重要的方法是:take put**<br />take方法<br />:获取并移除队列的头结点,如果无数据,阻塞,直到有数据<br />![1648737049(1).png](https://cdn.nlark.com/yuque/0/2022/png/1275320/1648737054175-090557d3-701f-4dd8-9442-9cdfbe6543e7.png#clientId=uc1abc99c-741a-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=223&id=u5e281681&margin=%5Bobject%20Object%5D&name=1648737049%281%29.png&originHeight=335&originWidth=723&originalType=binary&ratio=1&rotation=0&showTitle=false&size=17462&status=done&style=none&taskId=u9c8cb8b7-834f-490e-a762-1680c4eeafb&title=&width=482)<br />put方法<br /> 插入元素,如果已经满了,阻塞,直到可以插入<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/1275320/1648737097159-f811c0e3-5f92-4d0b-9c32-e363675da2db.png#clientId=uc1abc99c-741a-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=221&id=u73bc8746&margin=%5Bobject%20Object%5D&name=image.png&originHeight=332&originWidth=762&originalType=binary&ratio=1&rotation=0&showTitle=false&size=25053&status=done&style=none&taskId=u6114f564-5d1e-481e-ace7-993afa2b0a4&title=&width=508)<br />是否有界<br />无界:LinkedBlockingQueue 上线Integer。Max_VALUE<br />有界::初始化一个大小<br />应用场景<br />生产者,消费者<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/1275320/1648737229334-1b9e9d6d-4a6b-491d-b9f2-2d05a475dddf.png#clientId=uc1abc99c-741a-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=255&id=u55de0d1a&margin=%5Bobject%20Object%5D&name=image.png&originHeight=383&originWidth=752&originalType=binary&ratio=1&rotation=0&showTitle=false&size=33849&status=done&style=none&taskId=u75642ffe-1eaa-4122-85a2-255e0b7ea49&title=&width=501.3333333333333)1<br />1:线程安全<br />2:隔离作用,具体任务与执行任务类的解耦<br />提高安全性
  11. **常见阻塞队列**<br />BlockingQueue 接口的实现类都被放在了 juc 包中,它们的区别主要体现在存储结构上或对元素操作上的不同,但是对于takeput操作的原理,却是类似的。
  12. | **队列** | 描述 |
  13. | --- | --- |
  14. | **ArrayBlockingQueue** | 基于数组结构实现的一个有界阻塞队列 |
  15. | **LinkedBlockingQueue** | 基于链表结构实现的一个有界阻塞队列 |
  16. | **PriorityBlockingQueue** | 支持按优先级排序的无界阻塞队列 |
  17. | **DelayQueue** | 基于优先级队列(PriorityBlockingQueue)实现的无界阻塞队列 |
  18. | **SynchronousQueue** | 不存储元素的阻塞队列 |
  19. | **LinkedTransferQueue** | 基于链表结构实现的一个无界阻塞队列 |
  20. | **LinkedBlockingDeque** | 基于链表结构实现的一个双端阻塞队列 |
  21. ![31437.png](https://cdn.nlark.com/yuque/0/2022/png/1275320/1648738794347-803375a9-465e-4b64-97a8-50dffbd7e68f.png#clientId=uc1abc99c-741a-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=1442&id=u26ed7f40&margin=%5Bobject%20Object%5D&name=31437.png&originHeight=2163&originWidth=1815&originalType=binary&ratio=1&rotation=0&showTitle=false&size=446107&status=done&style=none&taskId=ubf80fdb1-86ea-4dd2-a71c-9aaf0abbbea&title=&width=1210)<br />[https://www.processon.com/view/link/618ce3941e0853689b0818e2#map](https://www.processon.com/view/link/618ce3941e0853689b0818e2#map)
  22. <a name="pdfL3"></a>
  23. # ArrayBlockingQueue
  24. - 有界
  25. - 数组
  26. - ReentrantLock
  27. - 入队,出队,同一把锁
  28. 场景:生产速度和消费速度基本匹配的情况下<br />生产速度远远大于消费速度,则会导致队列填满,大量生产线程被阻塞<br /> ![image.png](https://cdn.nlark.com/yuque/0/2022/png/1275320/1648818133458-ee95a534-6051-40dd-8fd9-ec5e6cea47a5.png#clientId=u0c4b7e7e-8333-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=187&id=ue292b4a8&margin=%5Bobject%20Object%5D&name=image.png&originHeight=280&originWidth=797&originalType=binary&ratio=1&rotation=0&showTitle=false&size=36497&status=done&style=none&taskId=ub8e2615e-8426-4336-a0d7-bdefde7fc61&title=&width=531.3333333333334)<br />**ArrayBlockingQueue使用**
  29. ```java
  30. BlockingQueue queue = new ArrayBlockingQueue(1024);
  31. queue.put("1"); //向队列中添加元素
  32. Object object = queue.take(); //从队列中取出元素

ArrayBlockingQueue的原理

数据结构
利用了Lock锁的Condition通知机制进行阻塞控制。
核心:一把锁,两个条件

  1. //数据元素数组
  2. final Object[] items;
  3. //下一个待取出元素索引
  4. int takeIndex;
  5. //下一个待添加元素索引
  6. int putIndex;
  7. //元素个数
  8. int count;
  9. //内部锁
  10. final ReentrantLock lock;
  11. //消费者
  12. private final Condition notEmpty;
  13. //生产者
  14. private final Condition notFull;
  15. public ArrayBlockingQueue(int capacity) {
  16. this(capacity, false);
  17. }
  18. public ArrayBlockingQueue(int capacity, boolean fair) {
  19. ...
  20. lock = new ReentrantLock(fair); //公平,非公平
  21. notEmpty = lock.newCondition();
  22. notFull = lock.newCondition();
  23. }

入队put方法

  1. public void put(E e) throws InterruptedException {
  2. //检查是否为空
  3. checkNotNull(e);
  4. final ReentrantLock lock = this.lock;
  5. //加锁,如果线程中断抛出异常
  6. lock.lockInterruptibly();
  7. try {
  8. //阻塞队列已满,则将生产者挂起,等待消费者唤醒
  9. //设计注意点: 用while不用if是为了防止虚假唤醒
  10. while (count == items.length)
  11. notFull.await(); //队列满了,使用notFull等待(生产者阻塞)
  12. // 入队
  13. enqueue(e);
  14. } finally {
  15. lock.unlock(); // 唤醒消费者线程
  16. }
  17. }
  18. private void enqueue(E x) {
  19. final Object[] items = this.items;
  20. //入队 使用的putIndex
  21. items[putIndex] = x;
  22. if (++putIndex == items.length)
  23. putIndex = 0; //设计的精髓: 环形数组,putIndex指针到数组尽头了,返回头部
  24. count++;
  25. //notEmpty条件队列转同步队列,准备唤醒消费者线程,因为入队了一个元素,肯定不为空了
  26. notEmpty.signal();
  27. }

出队take方法

  1. public E take() throws InterruptedException {
  2. final ReentrantLock lock = this.lock;
  3. //加锁,如果线程中断抛出异常
  4. lock.lockInterruptibly();
  5. try {
  6. //如果队列为空,则消费者挂起
  7. while (count == 0)
  8. notEmpty.await();
  9. //出队
  10. return dequeue();
  11. } finally {
  12. lock.unlock();// 唤醒生产者线程
  13. }
  14. }
  15. private E dequeue() {
  16. final Object[] items = this.items;
  17. @SuppressWarnings("unchecked")
  18. E x = (E) items[takeIndex]; //取出takeIndex位置的元素
  19. items[takeIndex] = null;
  20. if (++takeIndex == items.length)
  21. takeIndex = 0; //设计的精髓: 环形数组,takeIndex 指针到数组尽头了,返回头部
  22. count--;
  23. if (itrs != null)
  24. itrs.elementDequeued();
  25. //notFull条件队列转同步队列,准备唤醒生产者线程,此时队列有空位
  26. notFull.signal();
  27. return x;
  28. }

1648818550(1).png

LinkedBlockingQueue

  • 链表实现
  • 无界,最大Integer.MAX_VALUE
  • 两把锁

如果没有剩余内存,则队列将抛出OOM错误。所以为了避免队列过大造成机器负载或者内存爆满的情况出现,我们在使用的时候建议手动传一个队列的大小。
内部由单链表实现,只能从head取元素,从tail添加元素
采用两把锁的锁分离技术实现入队出队互不阻塞,添加元素和获取元素都有独立的锁,也就是说LinkedBlockingQueue是读写分离的,读写操作可以并行执行。
image.png

LinkedBlockingQueue使用

  1. //指定队列的大小创建有界队列
  2. BlockingQueue<Integer> boundedQueue = new LinkedBlockingQueue<>(100);
  3. //无界队列
  4. BlockingQueue<Integer> unboundedQueue = new LinkedBlockingQueue<>();

LinkedBlockingQueue的原理

  1. // 容量,指定容量就是有界队列
  2. private final int capacity;
  3. // 元素数量
  4. private final AtomicInteger count = new AtomicInteger();
  5. // 链表头 本身是不存储任何元素的,初始化时item指向null
  6. transient Node<E> head;
  7. // 链表尾
  8. private transient Node<E> last;
  9. // take锁 锁分离,提高效率
  10. private final ReentrantLock takeLock = new ReentrantLock();
  11. // notEmpty条件
  12. // 当队列无元素时,take锁会阻塞在notEmpty条件上,等待其它线程唤醒
  13. private final Condition notEmpty = takeLock.newCondition();
  14. // put锁
  15. private final ReentrantLock putLock = new ReentrantLock();
  16. // notFull条件
  17. // 当队列满了时,put锁会会阻塞在notFull上,等待其它线程唤醒
  18. private final Condition notFull = putLock.newCondition();
  19. //典型的单链表结构
  20. static class Node<E> {
  21. E item; //存储元素
  22. Node<E> next; //后继节点 单链表结构
  23. Node(E x) { item = x; }
  24. }

构造器

  1. public LinkedBlockingQueue() {
  2. // 如果没传容量,就使用最大int值初始化其容量
  3. this(Integer.MAX_VALUE);
  4. }
  5. public LinkedBlockingQueue(int capacity) {
  6. if (capacity <= 0) throw new IllegalArgumentException();
  7. this.capacity = capacity;
  8. // 初始化head和last指针为空值节点
  9. last = head = new Node<E>(null);
  10. }

入队put方法

  1. public void put(E e) throws InterruptedException {
  2. // 不允许null元素
  3. if (e == null) throw new NullPointerException();
  4. int c = -1;
  5. // 新建一个节点
  6. Node<E> node = new Node<E>(e);
  7. final ReentrantLock putLock = this.putLock;
  8. final AtomicInteger count = this.count;
  9. // 使用put锁加锁
  10. putLock.lockInterruptibly();
  11. try {
  12. // 如果队列满了,就阻塞在notFull上等待被其它线程唤醒(阻塞生产者线程)
  13. while (count.get() == capacity) {
  14. notFull.await();
  15. }
  16. // 队列不满,就入队
  17. enqueue(node);
  18. c = count.getAndIncrement();// 队列长度加1,返回原值
  19. // 如果现队列长度小于容量,notFull条件队列转同步队列,准备唤醒一个阻塞在notFull条件上的线程(可以继续入队)
  20. // 这里为啥要唤醒一下呢?
  21. // 因为可能有很多线程阻塞在notFull这个条件上,而取元素时只有取之前队列是满的才会唤醒notFull,此处不用等到取元素时才唤醒
  22. if (c + 1 < capacity)
  23. notFull.signal();
  24. } finally {
  25. putLock.unlock(); // 真正唤醒生产者线程
  26. }
  27. // 如果原队列长度为0,现在加了一个元素后立即唤醒阻塞在notEmpty上的线程
  28. if (c == 0)
  29. signalNotEmpty();
  30. }
  31. private void enqueue(Node<E> node) {
  32. // 直接加到last后面,last指向入队元素
  33. last = last.next = node;
  34. }
  35. private void signalNotEmpty() {
  36. final ReentrantLock takeLock = this.takeLock;
  37. takeLock.lock();// 加take锁
  38. try {
  39. notEmpty.signal();// notEmpty条件队列转同步队列,准备唤醒阻塞在notEmpty上的线程
  40. } finally {
  41. takeLock.unlock(); // 真正唤醒消费者线程
  42. }
  43. }

出队take方法

  1. public E take() throws InterruptedException {
  2. E x;
  3. int c = -1;
  4. final AtomicInteger count = this.count;
  5. final ReentrantLock takeLock = this.takeLock;
  6. // 使用takeLock加锁
  7. takeLock.lockInterruptibly();
  8. try {
  9. // 如果队列无元素,则阻塞在notEmpty条件上(消费者线程阻塞)
  10. while (count.get() == 0) {
  11. notEmpty.await();
  12. }
  13. // 否则,出队
  14. x = dequeue();
  15. c = count.getAndDecrement();//长度-1,返回原值
  16. if (c > 1)// 如果取之前队列长度大于1,notEmpty条件队列转同步队列,准备唤醒阻塞在notEmpty上的线程,原因与入队同理
  17. notEmpty.signal();
  18. } finally {
  19. takeLock.unlock(); // 真正唤醒消费者线程
  20. }
  21. // 为什么队列是满的才唤醒阻塞在notFull上的线程呢?
  22. // 因为唤醒是需要加putLock的,这是为了减少锁的次数,所以,这里索性在放完元素就检测一下,未满就唤醒其它notFull上的线程,
  23. // 这也是锁分离带来的代价
  24. // 如果取之前队列长度等于容量(已满),则唤醒阻塞在notFull的线程
  25. if (c == capacity)
  26. signalNotFull();
  27. return x;
  28. }
  29. private E dequeue() {
  30. // head节点本身是不存储任何元素的
  31. // 这里把head删除,并把head下一个节点作为新的值
  32. // 并把其值置空,返回原来的值
  33. Node<E> h = head;
  34. Node<E> first = h.next;
  35. h.next = h; // 方便GC
  36. head = first;
  37. E x = first.item;
  38. first.item = null;
  39. return x;
  40. }
  41. private void signalNotFull() {
  42. final ReentrantLock putLock = this.putLock;
  43. putLock.lock();
  44. try {
  45. notFull.signal();// notFull条件队列转同步队列,准备唤醒阻塞在notFull上的线程
  46. } finally {
  47. putLock.unlock(); // 解锁,这才会真正的唤醒生产者线程
  48. }
  49. }

LinkedBlockingQueue与ArrayBlockingQueue对比

LinkedBlockingQueue是一个阻塞队列,内部由两个ReentrantLock来实现出入队列的线程安全,由各自的Condition对象的await和signal来实现等待和唤醒功能。它和ArrayBlockingQueue的不同点在于:

  • 队列大小有所不同,ArrayBlockingQueue是有界的初始化必须指定大小,而LinkedBlockingQueue可以是有界的也可以是无界的(Integer.MAX_VALUE),对于后者而言,当添加速度大于移除速度时,在无界的情况下,可能会造成内存溢出等问题。
  • 数据存储容器不同,ArrayBlockingQueue采用的是数组作为数据存储容器,而LinkedBlockingQueue采用的则是以Node节点作为连接对象的链表。
  • 由于ArrayBlockingQueue采用的是数组的存储容器,因此在插入或删除元素时不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响。
  • 两者的实现队列添加或移除的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁,而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。