常用 BlockingQueue:ArrayBlockingQueue、LinkedBlockingQueue、DelayQueue、PriorityBlockingQueue、SynchronousQueue
2014拍摄于四川羌族藏族自治区郎木寺。
微信公众号
王皓的GitHub:https://github.com/TenaciousDWang
今天这一回,我们来说一下Java并发编程中的阻塞队列BlockingQueue。
阻塞队列BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利。
首先我们现在看一下什么是队列,以下是一个队列模型示例。
我们可以看到数据由队列的一端输入,从另外一端输出,常用的队列主要有以下两种:
1、先进先出(FIFO):在之前我们说AQS时,提出过FIFO这个概念,先插入的队列的元素也最先出队列,类似于排队的功能。从某种程度上来说这种队列也体现了一种公平性。
2、后进先出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发生的事件。
我们再来看一下阻塞队列,我们用一个经典的生产者与消费者的例子来说明,假设有若干生产线程,若干消费线程,生产者线程生产数据供给消费者来消费,如果在一定时间内,生产者的速度大于消费者的速度,那么就会产生过量的数据,这些数据需要被缓存起来,且当缓存装满时,生产者线程需要被挂起暂时停止生产,等待消费者线程将缓存里的数据取走。反之,生产者线程速度跟不上消费者速度则消费者线程应该挂起,等待生产者线程生产数据。
在JUC包没有发布之前,要解决这个问题就必须额外地实现同步策略以及线程间唤醒策略,这个实现起来就非常麻烦,所有的程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全。
JUC包发布之后,为我们带来了BlockingQueue阻塞队列,上面的生产者与消费者例子里,比如一个线程从一个空的阻塞队列中取元素,此时线程会被阻塞直到阻塞队列中有了元素。当队列中有元素后,被阻塞的线程会自动被唤醒(不需要我们编写代码去唤醒)。这样提供了极大的方便性。
根据在生产者和消费者里使用阻塞队列我们来用图片展示一下。
1、当队列中没有数据的情况下,消费者所有线程都会被自动阻塞,直到有数据放入队列。
2、当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞,直到队列中有空的位置,生产线程被自动唤醒。
我们先来看一下BlockingQueue中的核心方法,首先是插入数据:
1、offer(E e)表示如果可能的话,将e加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.
2、put(E e)把e加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续.
3、offer(E e, long timeout, TimeUnit unit)可以设定等待的时间,如果在指定的时间内,还不能往队列中加入,则返回失败。
接下来是获取数据:
1、poll()这个其实是BlockingQueue继承Queue的方法,取走BlockingQueue里排在首位的对象,如果队列是空的返回null。
2、poll(long timeout, TimeUnit unit)从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则知道时间超时还没有数据可取,返回失败。
3、take()检索并删除此队列的头,如有必要则等待,直到某个元素可用为止。
4、int drainTo(Collection super E> c)一次性从BlockingQueue获取所有可用的数据对象(int drainTo(Collection super E> c, int maxElements)还可以指定获取数据的个数),通过该方法,可以提升获取数据效率,不需要多次分批加锁或释放锁。
接下来我们来看一下用的多的几个主要阻塞队列,并用代码示例来说明如何使用。
1、ArrayBlockingQueue
基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。
ArrayBlockingQueue是一个由数组结构组成的有界阻塞队列,我可以从构造方法看到。必须指定数组大小,所有为有界。并且可以指定公平性与非公平性,默认情况下为非公平的,即不保证等待时间最长的队列最优先能够访问队列。第三个为可以指定在初始化时加入一个集合,基本没用过。
ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,生产者与消费者无法并行。
我们来写一个例子,首先创建一个生产者。
然后创建一个消费者。
最后创建一个测试类,来启动两个线程即可。
2.LinkedBlockingQueue
内部也维持着一个链表构成的数据缓冲队列LinkedBlockingQueue不同于ArrayBlockingQueue,它如果不指定容量,默认为Integer.MAX_VALUE,在这里,我们需要注意的是:如果构造一个LinkedBlockingQueue对象,而没有指定其大小,LinkedBlockingQueue会默认为Integer.MAX_VALUE,这样的话,如果生产者的速度大于消费者的速度,也许还没有等队列阻塞产生,系统内存就已经被消耗殆尽了。
同ArrayBlockingQueue不同,生产者端和消费者端分别采用了独立的锁来控制数据同步,内部由两个ReentrantLock来实现出入队列的线程安全,由各自的Condition对象的await和signal来实现等待和唤醒功能。这也意味着在高并发的操作下生产者和消费者可以并行的操作队列中的数据,依次来提高整个队列的并发性能。
ArrayBlockingQueue采用的是数组作为数据存储容器,而LinkedBlockingQueue采用的则是以Node节点作为连接对象的链表。 前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node对象。
来看一下LinkedBlockingQueue的put方法。
来看一下LinkedBlockingQueue的take方法。
最后我们来举一个例演示一下LinkedBlockingQueue的使用方法。
3.DelayQueue
DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,是一个无界的BlockingQueue。
因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
DelayQueue用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。这种队列是有序的,即队头对象的延迟到期时间最长。注意:不能将null元素放置到这种队列中。
Delayed,一种混合风格的接口,用来标记那些应该在给定延迟时间之后执行的对象。此接口的实现必须定义一个 compareTo 方法,该方法提供与此接口的 getDelay 方法一致的排序。
首先创建一个消息对象继承Delayed,并覆写compareTo与getDelay这两个方法。
然后我们创建一个消息消费者。
最后我们来创建一个延时队列,并添加消息,然后执行消费者线程。
添加两个消息体,id号实际决定运行顺序,1号消息在线程启动后1秒后打印,2号消息在1号打印2秒后打印,总共延时为三秒。
我们来看一下打印结果。
如果将id号颠倒,则三秒后打印world,并马上打印hello,由于hello被阻塞在world后面,且延时时间已到。
4. PriorityBlockingQueue
基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。由上可知是一个无界有序的阻塞队列。
一般使用优先级的队列的场景最常见的就是VIP排队,谁的VIP等级高,谁就排在前面,哪怕是后来进入队列的,这里我们来举一个例子演示一下优先级队列的使用。
首先创建一个Customer类,它包括姓名和VIP等级两个属性。
然后写一个比较客户VIP等级的类VipComparator
接下来,添加一个生产者队列随机生成客户放入优先级队列中。
再添加一个消费者队列,用来处理客户的排队请求。
最后我们创建一个测试类,在队列中添加10条数据,使用join完成后,开始执行消费者线程处理队列中的内容,看一下是否按照VIP等级顺序执行。
看一下打印结果:
VIP等级高的优先执行。
5、SynchronousQueue
一种无缓冲的等待队列,类似于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的最终消费者,而消费者必须亲自去集市找到所要商品的直接生产者,如果一方没有找到合适的目标,那么对不起,大家都在集市等待。相对于有缓冲的BlockingQueue来说,少了一个中间经销商的环节(缓冲区),如果有经销商,生产者直接把产品批发给经销商,而无需在意经销商最终会将这些产品卖给那些消费者,由于经销商可以库存一部分商品,因此相对于直接交易模式,总体来说采用中间经销商的模式会吞吐量高一些(可以批量买卖);但另一方面,又因为经销商的引入,使得产品从生产者到消费者中间增加了额外的交易环节,单个产品的及时响应性能可能会降低。
声明一个SynchronousQueue有两种不同的方式,它们之间有着不太一样的行为。公平模式和非公平模式的区别:
1、如果采用公平模式:SynchronousQueue会采用公平锁,并配合一个FIFO队列来阻塞多余的生产者和消费者,从而体系整体的公平策略。
2、但如果是非公平模式(SynchronousQueue默认):SynchronousQueue采用非公平锁,同时配合一个LIFO队列来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。
这里写一个Demo来一下如何使用:
输出结果为:
SynchronousQueue 内部没有容量,但是由于一个插入操作总是对应一个移除操作,反过来同样需要满足。那么一个元素就不会再SynchronousQueue 里面长时间停留,一旦有了插入线程和移除线程,元素很快就从插入线程移交给移除线程。也就是说这更像是一种信道(管道),资源从一个方向快速传递到另一方 向。显然这是一种快速传递元素的方式,也就是说在这种情况下元素总是以最快的方式从插入着(生产者)传递给移除着(消费者),这在多任务队列中是最快处理任务的方式。在线程池里的一个典型应用是Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。
以上就是常用的几种阻塞队列的使用展示及介绍。