阻塞队列

来源:互联网 发布:gta5卡怎么优化 编辑:程序博客网 时间:2024/06/06 12:48

首先我们来看这样一个应用场景。假如现在有一个系统有两端,一端负责发送指令(比如扣款指令,放款指令),一端负责接受指令并执行相应的操作。这两个功能应该作为两个单独的线程在后台一直执行,以便及时的发送和处理消息。为了达到这个目的,我们必须设置两个一直轮循的线程,和一个用来存储发送端发送的指令的队列。假如现在消费者线程由于要执行完相应的指令操作(比如扣款,放款等)导致从队列中消费数据的速度远小于生产端发送的数据,当队列积累数据过多,导致队列容量全部用尽,这时如果生产端继续发送指令的话,由于队列已经存储不了多余的指令,就会抛出IllegalStateException异常。为了解决这一问题,现在有两种方法:

         1.每次生产或消费数据时,先判断队列中是否有可用空间和资源,如果没有就一直轮循,直到资源可用

         2.每次生产或消费数据时。先判断队列中是否有可用空间和资源,如果不可用就阻塞当前线程,直到资源可用。

在大型系统中,可能存在很多的消费者和生产者线程模型,如果采用第一条方案,一直轮循的话,就会大量的资源浪费(自旋锁也是这个原理),所以自从jdk1.5之后,官方的并发包中就实现了阻塞队列,帮助我们省略复杂的在不同情况下线程通信和阻塞唤醒等操作,这一切,通过阻塞队列,就可以自动帮我们完成。

java.util.concurrent包下提供了一个基本接口:BlockingQueue 。通过实现这个接口,可以定制出各种情况下不同用途的阻塞队列。我们先来看看该接口的基本方法:

     produce:

            1.boolean add(E e):  如果没有容量限制,立即插入这条元素,如果当前没有可用的资源,就抛出一个异常

            2.boolean offer(E e); 如果没有容量限制,立即插入这条元素,如果当前没有可用的资源,就返回false

            3.boolean offer(E e, long timeout, TimeUnit unit) 如果资源不可用等待指定时间,超时就返回false

            4.void put(E e);   插入指定的元素到队列中,如果有必要的话会一直阻塞直到资源可用

     consume:

            1.E take():  获取并移除队首元素,如果有必要会一直等待直到资源可用

            2.E poll(long timeout, TimeUnit unit): 获取并移除队首元素,如果资源不可用,等待指定超时时间。

            3.int drainTo(Collection c)一次性取出队列中的所有元素,添加进给定的collection中

以上只是对该接口中的方法的一个基本归纳。下面我们来看看阻塞队列的众多实现类的细节:


1.ArrayBlockingQueue

       这是一个基于数组的有界的阻塞队列,内部使用一个定长数组来存储添加进队列的元素。内部采用FIFO(先进先出)的方式来对元素进行排序,队尾元素是在队列中存在时间最短的,反之队首元素在队列中存在时间最长。一旦数组的长度指定了,就不能进行扩展。当向一个完整队列中插入一个元素和向空队列中取一个元素都会造成当前的执行此操作的线程阻塞。该阻塞队列对等待的producer和consumer线程支持可选的公平策略来进行排序。默认情况下,这些等待中的线程的顺序是不能保证的。但如果在构建该对象时,指定排序策略fairness为true,则能保证线程按照先进先出的顺序进行访问。当采用公平锁时,虽然降低了整个系统的吞吐量,但在一定程度上避免了线程饥饿(一个线程长时间得不到cpu资源)。

2.DelayQueue:

      这时一个基于延时的无界的阻塞队列,内部使用PriorityQueue优先级队列来保存元素,PriorityQueue内部又使用一个平衡二叉堆来保存元素。可以先下一个不怎么精准的定义,DelayQueue是一个使用PriorityQueue(优先级根据超时时长来决定)来实现BlockingQueue的阻塞队列。该阻塞队列只接受实现了Delayed的类的实例。Delayed接口中有两个方法,一个是继承自Comparable的compareTo方法,用来根据延时时长进行排序的,还有一个是本身的getDelay方法,获取距离指定的时延还有多长时间。有一点要注意的是,该阻塞队列是无界的,也就是说生产者可以一直push元素进来,而不用担心阻塞,但如果消费者要拿到队列中的元素的话,就必须等该元素设置的延时过期。队首的元素是延时过期时间最长的元素。这个队列在管理连接池时十分方便,比如在实现自己的数据库连接池时,就可以将自定义的连接对象实现Delayed接口,内部维护一个真正的连接对象,然后将所有自定义的连接放进队列中,设置时延,超过指定时间就进行销毁。除此之外,还可以用来处理一些延迟任务或者用来管理缓存。

3.LinkedBlockingQueue:

     这时一个基于链表的可选界限的阻塞队列,链表节点类型为Node,该队列维护了链表的链首和链尾。该阻塞队列在构建时,可以指定容量大小,如果不指定,默认为Integer的MAX_VALUE,也就是2的32次方-1. 因此如果生产者的生产速度远大于消费者的消费速度的话,如果不指定容量,则可能会发生内存溢出。基于链表的队列比基于数组的队列具有更大的吞吐量,但往往在高并发应用中的性能表现具有很低的可预测性。由于该阻塞队列采取分离锁(ArrayBlockingQueue的消费者和生产者共用一个锁对象),所以生产端和消费端可以真正的做到并行,这也是该阻塞队列具有更大的吞吐量的一个原因。但由于是采用链表装载元素,当有大量的数据要存储时,又会产生的大量的Node对象,对于性能又有一定的影响,所以导致很难预测该队列在高并发的应用中的具体的性能表现。

4.PriorityBlockingQueue:

     基于优先级的无界的阻塞队列。和DelayQueue一样,内部使用平衡二叉堆来存储元素。内部使用comparator方法来进行优先级的排序。默认初始容量大小为11,超过该容量后将会扩容,没有容量大小限制。可用于对一些具有优先级的任务进行排序然后执行执行。

5.SynchronousQueue:

     该阻塞队列不保存任何缓存元素。当执行插入一个元素的操作时,会一直等待另一个线程的相应的移除操作,反之亦然。一个SynchronousQueue是没有内部容器来缓存元素的,即使是一个元素。执行peek操作时会直接返回null,因为元素只有当你想要移除的时候才回存在,也就是说只有当你想要remove,才会insert一个元素。如果一个执行中的线程的对象和一个其他执行的线程中的对象进行同步,来传递一些信息,事件或者任务,这是一个典型的握手模式,这时使用SynchronousQueue是非常好的选择。在Executor的newCachedThreadPool方法中就是用了这个队列。


以上只是一些阻塞队列的概览,具体用法和细节,如果有时间参考自己的经验和书籍会做一些其他的说明。

原创粉丝点击