当前位置: 首页>后端>正文

java 中的阻塞队列

一. 什么是队列

队列是一种特殊的线性表, 特殊之处在于它只允许在表的前端 (front) 进行删除操作, 而在表的后端 (rear) 进行插入操作. 和栈一样, 队列是一种操作受限制的线性表. 进行插入操作的端称为队尾, 进行删除操作的端称为对头.

在队列中插入一个队列元素称为入队, 从队列中删除一个队列元素称为出队. 因为队列只允许在一端插入, 在另一端删除, 所以只有最早进入队列的元素才能最先从队列中删除. 故队列又称为先进先出 (FIFO - first in first out) 线性表.


java 中的阻塞队列,第1张
队列示意图.png

?

二. 什么是阻塞队列

简单来说就是支持阻塞插入和阻塞移除的队列就可被称为阻塞队列.

  • 阻塞插入
    意思是当队列满的时候, 队列会阻塞插入元素的线程, 直到队列有空缺. 才可以继续插入元素.

  • 阻塞移除
    当队列为空的时候, 获取队列元素的线程会一直等待队列变为非空.

阻塞队列常用于生产者和消费者的场景, 生产者是向队列里添加元素的线程, 消费者是从队列里取元素的线程. 阻塞队列就是生产者用来存放元素, 消费者用来获取元素的容器.

在线程世界里, 生产者就是生产数据的线程, 消费者就是消费数据的线程. 在多线程开发中, 如果生产者处理速度很快, 而消费者处理速度很慢, 那么生产者就必须等待消费者处理完, 才能继续生产数据. 同样道理, 如果消费者的处理能力大于生产者, 那么消费者也就必须等待生产者.

为了解决这种生产消费能力不均衡的的问题, 便有了生产者和消费者模式. 生产者和消费者通过一个容器来解决生产者和消费者的强耦合问题. 生产者和消费者彼此之间不直接通信, 而是通过阻塞队列来进行通信, 所以生产者生产完数据之后不用等待消费者处理, 直接丢给阻塞队列. 消费者则不需要找生产者要数据, 而是直接从阻塞队列里取. 阻塞队列就相当于一个缓冲区, 平衡了生产者和消费者的处理能力.
?

三. 常用阻塞队列

JDK 为我们提供了一个继承自 Queue 接口的 BlockingQueue 接口 , 常用的阻塞队列几乎都实现了此接口. 这些方法并非都是阻塞类的方法, 还包含了非阻塞类的方法. 这两种方法基本都是成对出现的.例如插入和获取.

java 中的阻塞队列,第2张
Queue
java 中的阻塞队列,第3张
BlockingQueue
方法/处理方式 抛出异常 返回特殊值 一直阻塞 超时退出
插入方法 add(E) offer(E) put(E) offer(E, long, TimeUnit)
移除方法 remove() poll() take() poll(long, TimeUnit)
检查方法 element() peek()

真正体现了阻塞的两个方法就是 put(E) / take()

  • 抛出异常
    当队列满的时候, 如果再向队列中插入元素, 会抛出默认的 IllegalStateException 异常.
    当队列为空时候, 如果再从队列中获取元素, 会抛出 NoSuchElementException 异常

  • 返回特殊值
    每次对队列执行插入操作, 会返回元素是否插入成功, 成功则返回 true.
    如果是获取操作, 则是从队列中获取一个元素, 没有这个元素的话, 返回值为 null.

  • 一直阻塞
    当队列满的时候, 如果生产者线程继续向队列中 put 元素, 队列将会一直阻塞生产者线程, 直到队列可用或者响应中断退出.
    当队列为 null 的时候, 如果消费者线程从队列中 take 元素, 队列会阻塞住消费者线程, 直到队列不为 null.

  • 超时退出
    当阻塞队列满时, 如果生产者线程继续向队列中插入元素, 队列会阻塞生产者线程一段时间, 如果超过了这个指定的时间, 生产者线程就会退出.

?

JDK 中实现了 BlockingQueue 接口的阻塞队列如下图所示

java 中的阻塞队列,第4张
  • ArrayBlockingQueue: 一个由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列。
  • PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列。
  • DelayQueue: 一个使用优先级队列实现的无界阻塞队列。
  • SynchronousQueue: 一个不存储元素的阻塞队列。
  • LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列。
  • LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。

以上的阻塞队列都实现了 BlockingQueue 接口, 同时也都是线程安全的. (其中 LinkedBlockingDeque 实现了 BlockingDeque 接口. LinkedTransferQueue 实现了 TransferQueue 接口).

有界队列与无界队列的概念
  • 有界队列就是长度有限, 满了以后生产者就会阻塞. 无界队列就是队列中能放无数的东西, 而不会因为队列长度限制被阻塞. 当然还是会有限制的, 限制来源于系统资源的限制, 如果处理不及时, 导致队列越来越大超出一定的限制使内存超限, 会被操作系统或者 JVM 直接杀掉.

  • 其实无界队列也会阻塞, 因为阻塞不仅体现在生产者放入元素的时候会阻塞, 消费者拿元素的时候, 如果没有队列中没有元素, 同样也会阻塞.

1. ArrayBlockingQueue
  • 是一个用数组实现的有界阻塞队列. 此队列按照先进先出(FIFO) 的原则对元素进行排序. 默认情况下不保证线程公平的访问队列, 所谓公平访问队列是指阻塞的线程, 可以按照阻塞的先后顺序访问队列, 即先阻塞线程先访问队列. 非公平性是对先等待的线程是非公平的, 当队列可用时, 阻塞的线程都可以争夺访问队列的资格, 有可能先阻塞的线程最后才访问队列. 初始化时有参数可以设置.
2. LinkedBlockingQueue
  • 是一个用链表实现的有界阻塞队列, 此队列的默认和最大长度为 Integer.MAX_VALUE. 此队列按照先进先出的原则对元素进行排序.
3. PriorityBlockingQueue
  • 是一个支持优先级的无界阻塞队列. 默认情况下元素才去自然顺序升序排列. 也可以自定义类实现 compareTo() 方法来指定元素的排序规则, 或者初始化 PriorityBlockingQueue 时, 指定构造参数 Comparator 来对元素进行排序. 需要注意的是不能保证同优先级元素的顺序.
4. DelayQueue
  • 是一个支持延迟获取元素的无界阻塞队列. 队列使用 PriorityQueue 来实现. 队列中的元素必须实现 Delayed 接口. 在创建元素时可以指定多久才能从队列中获取当前元素. 只有在延迟期满的时候才能从队列中提取元素.
5. SynchronousQueue
  • 是一个不存储元素的阻塞队列. 每一个 put 操作必须等待一个 take 操作. 否则不能继续添加元素. 可以理解为 SynchronousQueue 负责把生产者线程处理的数据直接传递给消费者线程. 队列本身不存储任何元素, 适合传递类的场景.

  • SynchronousQueue 的吞吐量高于 ArrayBlockingQueueLinkedBlockingQueue

6. LinkedTransferQueue
  • 多了 transfertryTransfer 方法

  • transfer: 如果当前有消费者线程正在等待接收数据 (消费者线程使用 take() 方法或带时间限制的 poll() 方法时) , transfer 方法可以把生产者传入的元素立刻传递给消费者线程. 如果没有消费者在等待接收元素, transfer 方法会将元素存放在队列的 tail 节点, 并等到该元素被消费者消费了才返回.

  • tryTransfer: 这个方法用来试探生产者传入的元素是否可以直接传递给消费者. 如果没有消费者等待接收元素则返回 false. 和 transfer 方法的区别是 tryTransfer 方法无论是否有消费者接收, 方法立即返回. 而 transfer 方法是必须等到消费者消费了才返回.

7. LinkedBlockingDeque
  • 是一个链表结构的双向阻塞队列. 所谓双向队列指的是可以从队列的两端插入和移除元素. 双向队列因为多了一个操作队列的入口, 在多线程同时入队时, 也就减少了一半的竞争.

  • 多了 addFirst, addLast, offerFirst ....等方法. 插入方法 add 等同于 addLast, 移除 remove 等同于 removeFirst. 但是 take 方法却等同于 takeFirst.

  • 在初始化 LinkedBlockingDeque 时可以设置其容量防止过度膨胀.

Array 实现的队列和 Linked 实现队列的区别
  1. 队列中锁的实现不同
    • ArrayBlockingQueue实现的队列中的锁是没有分离的, 即生产和消费用的是同一个锁
    • LinkedBlockingQueue 实现的队列中的锁是分离的, 即生产用的是 putLock, 消费是 takeLock.
  2. 在生产或消费时操作不同
    • ArrayBlockingQueue 实现的队列中在生产和消费的时候, 是直接将枚举对象插入或移除的.
    • LinkedBlockingQueue 实现的队列中在生产和消费的时候, 需要把枚举对象转换为 Node<E> 进行插入或者移除. 会影响性能.
  3. 队列大小初始化方式不同
    • ArrayBlockingQueue 实现的队列中必须制定队列的大小.
    • LinkedBlockingQueue 实现的队列中可以不指定队列的大小, 但是默认是 Integer.MAX_VALUE.

在 Android 开发中会使用到的估计也就只有 ArrayBlockingQueueLinkedBlockingQueue. 剩下的几种感兴趣的也可以了解一下.


https://www.xamrdz.com/backend/3gm1936015.html

相关文章: