1 什么是阻塞队列BlockingQueue
1.1 阻塞队列
java.util.concurrent 包里的 BlockingQueue是一个接口, 继承Queue接口,Queue接口继承 Collection
BlockingQueue----->Queue–>Collection
1、非阻塞队列的问题
我们常用的非阻塞队列,比如PriorityQueue、LinkedList(LinkedList是双向链表,它实现了Dequeue接口)。他们有一个很大问题就是:它不会对当前线程产生阻塞,那么在面对类似消费者-生产者的模型时,就必须额外地实现同步策略以及线程间唤醒策略,这个实现起来就非常麻烦。
2、阻塞队列的好处
阻塞队列会对当前线程产生阻塞,比如一个线程从一个空的阻塞队列中取元素,此时线程会被阻塞直到阻塞队列中有了元素。当队列中有元素后,被阻塞的线程会自动被唤醒(不需要我们编写代码去唤醒)。这样提供了极大的方便性。
---->通知模式:
当生产者往满的队列添加元素时会-------->阻塞住生产者
当消费者消费了一个队列里的元素后=---------->会通知当前队列可用(例如Condition实现)
1.2 BlockingQueue的方法+处理方式
三种操作: 插入、移除、检查
四组处理方式: 抛出异常、返回特殊值、一直阻塞、超时退出
(三种操作,每种操作都有四种处理方式可以选择。
如果请求的操作不能得到立即执行的话,每个方法的处理方式也不同。)
操作\ 处理方式 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
插入 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
移除 | remove() | poll() | take() | poll(time, unit) |
检查 | element() | peek() | 不可用 | 不可用 |
2 java里的阻塞队列
2.1 BlockingQueue接口
在此基础上,BlockingQueue定义了如下几个阻塞方法:
入队方法: put (E e) 、 offer(E e,超时时间,时间单位)
出队方法:take(E e) 、poll(E e,超时时间,时间单位)
2.2 BlockingQueue分类
(1)有界阻塞队列——ArrayBlockingQueue和LinkedBlockingQueue
(2)优先无界阻塞队列——PriorityBlockingQueue
(3)同步阻塞队列——SynchronousQueue
(4)延时阻塞队列——DelayQueue
1、有界阻塞队列
入队时:如果已满,wait ; 当有出队时,会通知可以入队
出队时:如果空,wait; 当有入队时,回通知可以出队
- ArrayBlockingQueue
一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。队列的头部 是在队列中存在时间最长的元素。队列的尾部 是在队列中存在时间最短的元素。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。
这是一个典型的“有界缓存区”,固定大小的数组在其中保持生产者插入的元素和使用者提取的元素。一旦创建了这样的缓存区,就不能再增加其容量。试图向已满队列中放入元素会导致操作受阻塞;试图从空队列中提取元素将导致类似阻塞。此类支持对等待的生产者线程和使用者线程进行排序的可选公平策略。默认情况下,不保证是这种排序。然而,通过将公平性 (fairness) 设置为 true 而构造的队列允许按照 FIFO 顺序访问线程。公平性通常会降低吞吐量,但也减少了可变性和避免了“不平衡性” - LinkedBlockingQueue
链表实现的的有界阻塞队列,默认和最大长度=Integer.MAX_VALUE(容易耗尽内存,使用时需要指定大小)
不同点:
2、优先无界阻塞队列
- PriorityBlockingQueue (理论上无界)
----可以指定优先级来排序的无界阻塞队列
默认:元素按照自然顺序升序排列。在该队列中添加了一个Comparator比较器用于排序,但是不保证具有同等优先级的元素的顺序。在创建时指定的容量只是一个初始容量,随着元素的不断增多,其会调用tryGrow方法进行扩容。此类及其迭代器可以实现 Collection 和 Iterator 接口的所有可选方法。iterator() 方法中提供的迭代器并不保证以特定的顺序遍历 PriorityBlockingQueue 的元素。如果需要有序地进行遍历,则应考虑使用 Arrays.sort(pq.toArray())。此外,可以使用方法 drainTo 按优先级顺序移除 全部或部分元素,并将它们放在另一个 collection 中。
扩容:首先会释放锁,然后计算新数组的容量,然后再为主线程加锁的方式进行。其中,容量过大可能会溢出,抛出OOM异常。
入队:使用堆排序算法对元素进行排序。如果元素的数量超过了当前的容量,则首先进行扩容操作;然后,会根据比较器进行插入操作。其会调用队列中的siftUpComparable或siftUpUsingComparator方法插入数据
出队:使用堆排序算法对元素进行排序,根据排序的元素顺序进行出队
3、同步阻塞队列
- SynchronousQueue
一个同步阻塞队列,其本身并不保存元素。它的内部同时只能够容纳单个元素,每个入队PUT操作都必须等待一个出队Take操作,否则不能继续添加元素。默认非公平访问队列策略,可设为公平。
公平访问队列:指阻塞的线程,可以按照阻塞的先后顺序访问队列,即先阻塞线程先访问队列。
非公平访问队列:对先等待的线程是非公平的,当队列可用时,阻塞的线程都可以争夺队列的资格,有可能先阻塞的线程最后才访问队列。
4、延时阻塞队列
- DelayQueue
Delayed 元素的一个无界阻塞队列,队列用优先队列实现,在创建时可以指定多久才能从队列中获取当前元素,只有延时期满时才能从队列中提取元素。
该队列的头部 是延迟期满后保存时间最长的 Delayed 元素。如果延迟都还没有期满,则队列没有头部,并且 poll 将返回 null。当一个元素的 getDelay(TimeUnit.NANOSECONDS) 方法返回一个小于等于 0 的值时,将发生到期。即使无法使用 take 或 poll 移除未到期的元素,也不会将这些元素作为正常元素对待。例如,size 方法同时返回到期和未到期元素的计数。此队列不允许使用 null 元素
应用场景
缓存系统的设计:可以用延时阻塞队列保存元素的有效期,使用一个线程查询队列, 一旦能从队列中获取元素时,表示缓存有效期到了。
定时任务调度:可以用延时阻塞队列保存当天将会执行的任务和执行时间,一旦能从队列中获取任务就开始执行(TimeerQueue就是用其实现)。
3 阻塞队列的应用
1、在并发编程中,一般推荐使用阻塞队列,这样实现可以尽量地避免程序出现意外的错误。
2、阻塞队列使用最经典的场景就是socket客户端数据的读取和解析,读取数据的线程不断将数据放入队列,然后解析线程不断从队列取数据解析。还有其他类似的场景,只要符合生产者-消费者模型的都可以使用阻塞队列。