java 中的阻塞队列

一. 什么是队列

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

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


队列示意图.png

 

二. 什么是阻塞队列

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

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

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

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

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

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

三. 常用阻塞队列

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

Queue

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 接口的阻塞队列如下图所示

image.png
  • 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 进行插入或者移除. 会影响性能.
  3. 队列大小初始化方式不同
    • ArrayBlockingQueue 实现的队列中必须制定队列的大小.
    • LinkedBlockingQueue 实现的队列中可以不指定队列的大小, 但是默认是 Integer.MAX_VALUE.

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

你可能感兴趣的:(java 中的阻塞队列)