Java并发编程基础构建模块(03)——阻塞队列

        容器中还有一种数据结构十分有用,就是队列,实现了FIFO(公平性)或者LIFO(处理最近发生的事)的操作,解决了很多数据传输,任务分配等方面问题。多线程环境下,如何更“高效、安全”是最主要的问题,好在JDK提供了BlockingQueue,阻塞队列,极大的方便了我们的操作。

        队列最主要的3个方法:插入(插入一个元素)、移除(获得并删除)、检查(获得不删除)这3个操作。普通队列为每种方法提供了2种实现,当操作失败时(比如没有元素,却执行了删除;队列满了,再添加元素等),一种会抛出异常,另一种会返回false:

        

        而阻塞队列额外新增了2种实现方式,一种是阻塞,挂起当前线程,直到可以操作为止;另一种是超时,其实就是阻塞和返回特殊只的合体,在一定时间内阻塞,时间内可以操作则操作,超过指定时间扔不能操作,则返回特殊值。

        

        多线程情况下,使用阻塞队列极容易实现数据共享,如非常经典的生产者和消费者模式。生产者和消费者没有任何耦合,生产者只需要往队列放入数据,消费者只需要从队列获取数据然后处理,双方相互不依赖,当没有数据可以消费时,消费者阻塞,等待一旦有数据马上处理,生产者也是这样,当队列满了,阻塞等待可以放入数据为止。

        比如使用生产者消费者模式,做一个数据传输功能,数据库中被修改过的记录需要传输到其他系统,涉及的还有附件等信息,所以传输1条大约10秒,由于很多种情况修改数据,导致无法做到修改后马上传输,只能定时扫描哪些数据需要传输。示例如下:

        数据扫描类(生产者):

        

        数据传输类(消费者):

        

        定时任务调用者:

        

        其实这个示例也能看出来,生产者和消费者的数量是需要根据实际情况调整的,如果生产速率超过了消费速率,则工作就会积累,消耗额外内存(这也是为什么上面对象设置了1000大小),如果消费速率超过生产速率,则资源得不到有效利用。

        上面我们用到了ArrayBlockingQueue,其实接口BlockingQueue主要有5个实现类:

        1、 ArrayBlockingQueue:基于数组实现的阻塞队列,必须初始化一个大小才能使用,超过大小再放入元素则阻塞或失败。默认情况不是按照FIFO这种模式的,可以设置成严格按照FIFO这种公平的模式,不过公平性会降低吞吐量。写入数据和获取数据使用了一个锁,所以两者不能并行运行。(摘抄:DougLea之所以没这样去做,也许是因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。)

        2、 LinkedBlockingDeque:基于链表实现的阻塞队列,可以设置大小,如果不设置,默认大小为Integer.MAX_VALUE,所以要小心生产速率超过消费速率时,有可能造成内存溢出。写入数据和获取数据使用2个锁,所以两者可以完全可以并行运行。还有一点比较重要,LinkedBlockingDeque可以双向操作,是一个阻塞双端队列。

        ArrayBlockingQueue和LinkedBlockingQueue是两个最普通也是最常用的阻塞队列,类似ArrayList和LinkedList,一般情况下,在处理多线程间的生产者消费者问题,使用这两个类足以。

        3、 DelayQueue:延迟阻塞队列,放入队列中的元素只有过了设定的时间才能取出,是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。使用较少,一旦使用却非常好用,比如用于处理缓存对象,超过多久还没被用到的缓存对象就清除,或者处理超时未响应的连接队列等。

        4、 PriorityBlockingQueue:基于自然顺序的阻塞队列,也就是说可以通过Comparable进行排序,没有大小限制,也就是并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者,因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。

        5、 SynchronousQueue:没有缓冲的阻塞队列,相当于大小永远为0(其实没有容量),无法进行获取但不删除(如peek)等操作。相比有缓冲的阻塞队列,吞吐量有所降低(有缓冲的生产消费可以一对多,多对一,除非生产速率和消费速率完全相同,相比较吞吐量才会一样),但是响应时间是最快的(没有缓冲了嘛)。SynchronousQueue支持公平锁,默认非公平。非公平锁情况下,如果生产速率和消费速率不一致,再是LIFO队列,则容易造成某些生产者和消费者永远得不到处理,此处需要小心

        作为并发编程基础构建块的容器,BlockingQueue不单具备完整队列所有的基本功能,同时在多线程环境下,自动管理了多线间的自动等待与唤醒等功能,使得开发人员可以忽略这些底层细节,关注更高级的功能。与BlockingQueue类似的,还有BlockingDeque(双端队列),BlockingDeque实现了双端队列,能够在队列头尾进行高效的插入和删除操作。

        工作密取适合于既是消费者也是生产者的情况,正如阻塞队列适用于生产者消费者模式一样,双端队列适用于工作密取,在生产者消费者中,消费者就是消费者,所有消费者共享一个工作队列,而工作密取中,每个消费者都有各自的双端队列(一端生产,一端消费)。当一个工作线程处理数据时(此时是消费者),发现了更多的任务(变身成生产者,生产了更多的任务),然后将任务放到自己消费队列中(工作共享模式中是别人的),当自己的双端都处理完时(自己的双端队列为空),再帮助其他线程处理(从其他线程尾部处理,避免了队列竞争),这样每个线程都处于忙碌状态,效率非常高。如网络爬虫,处理了一个网页时,发现里面更多的链接,更多的页面需要处理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章