1.Queue框架
2.生产者消费者模式与阻塞队列
3.阻塞队列实现类
1.Queue框架
Java的集合类提供了Queue队列的实现,可以分为四个接口来对应相应的队列功能:
Queue:普通队列,头出尾进,一般用于满足FIFO先进先出功能。
Deque:双端队列,双端进出,如果需要实现LIFO后进先出的堆栈功能,优先考虑Deque的实现类,而非Stack接口。
BlockingQueue:阻塞队列,提供带阻塞功能的存入取出操作,头出尾进,用于实现生产者消费者模式。
BlockingDeque:双端阻塞队列,用于更复杂的生产者消费者模型。
1.Queue接口
队列的顶级接口,提供了队列的基本操作方法。
两套插入取出操作:
add/remove/element
boolean add(E e) | E remove() | E element() |
从队尾添加一个元素 | 从队头取出一个元素 | 返回队头元素,但不在队列中删除 |
offer/poll/peek
boolean offer(E e) | E poll() | E peek() |
从队尾添加一个元素 | 从队头取出一个元素 | 返回对头元素,但不在队列中删除 |
队列提供Collection的插入、提取、检查操作,区别是,add()与remove()操作失败时,会抛出异常,即不允许在队列满时插入在队列空时取出。而offer()在满队列插入时返回false,poll()在空队列取出时返回null。
如果使用链表系的实现,则队列无界,不会出现不允许插入的情况,如果使用数组系的实现,则看具体会不会自动扩容。
如果要使用队列,则考虑LinkedList(无界)与PriorityQueue(自动扩容)。
2.Deque接口
双端队列即在Queue基础上改为了首尾都能插入取出,同样是两套方法,
addFirst(E e) | E removeFirst() | peekFirst() |
从队头插入 | 从队头取出 | 检查 |
用双端队列来实现堆栈:
Deque<E> stack = new ArrayDeque<E>();
主要实现即Deque的ArrayDeque,和BlockingDeque的LinkedBlockingDeque。
3.BlockingQueue接口
阻塞队列提供了两个附加操作,即在队列满时阻塞添加,和队列空时阻塞取出。
所以在队列满的时候,添加操作会暂停直到能够添加的时候才执行,队列空的取出操作也是一样。
阻塞 | 超时 | |
插入 | put(E e) | offer(e,time,unit) |
取出 | E take() | E poll(time,unit) |
2.生产者-消费者模式
在实际的软件开发过程中,经常会碰到如下场景:某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、线程、进程等)。产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者。
单单抽象出生产者和消费者,还够不上是生产者/消费者模式。该模式还需要有一个缓冲区处于生产者和消费者之间,作为一个中介。生产者把数据放入缓冲区,而消费者从缓冲区取出数据。
特点:
1.解耦合
假设生产者和消费者分别是两个类。如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合)。将来如果消费者的代码发生变化,可能会影响到生产者。而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低了。
2.支持并发
生产者如果已经生产出来产品(数据)等待消费者使用,如果是直接耦合,需要消费者来接收的话,则生产者会拿着产品一直进入等待状态(阻塞),直到消费者拿走,那么如果消费者自身陷入了一段长时间的操作,生产者这边一直阻塞就会有很大的性能损耗。
而如果有缓冲区的话,则生产者只需要将产品往缓冲区放,就可以继续去生产下一个产品,这种情况主要是在生产者和消费者的处理速度不同的情况下,能够有很大的改善。
说明:
现有生产者1,生产产品1,消费者1,消费产品1.
1.生产者1与消费者1直接耦合时
生产者1生产了一个产品1,而此时消费者1暂时不需要,则生产者1一直等待。
突然,消费者1需要大量的产品时,则拿走一开始生产的产品1,生产者开始继续生产,但消费者很快就用完了产品1,则只能等待生产者一个一个的生产。
2.生产者1与消费者1用普通队列连接时
生产者1生产出产品,不需要等待消费者,直接放进队列里,继续生产,继续放进。
消费者需要产品时,直接在队列里拿,如果消费者使用产品的速度很快,只要队列中还有产品,可以一直拿,不需要等待生产者的即时生产。
问题:当生产者不断生产而消费者不拿时,缓冲的队列会满,这时,涉及到实现问题,可选的就两种,要么放弃要么阻塞,但放弃这个产品就为了继续生产,显然有些无用,所以可以选择让生产者进入类似直接耦合一样的阻塞状态,只是等待的时缓冲队列不满,这也就是阻塞队列的实现。
而当消费者在取产品遇到了空队列时也是如此,不可能取消本次操作,则同样陷入阻塞,等待队列不为空时再取产品。
3.阻塞队列实现
1.ArrayBlockingQueue
使用数组实现的阻塞队列。采取FIFO,新元素插入到队尾,获取则是从头部获取元素。
属于有界缓冲队列,队列的大小固定,初始化之后就不能更改了,因为大小固定,所以生产者和消费者都会有缓冲情况,即队列为空时获取会阻塞,队列已满时插入会阻塞。
构造器:
ArrayBlockingQueue<E>(int capacity);
常用方法:
put(E e);//阻塞放入元素
E take();//阻塞取出元素
实际上只有put()和take()方法是阻塞实现的插入和取出,(底层通过Lock的Condition来实现)
在阻塞队列中用add()和remove()也是可以的,在为空为满时报错而不是阻塞
不过在生产者消费者模式中一般都用阻塞方法
2.LinkedBlockingQueue
使用链表实现的阻塞队列。
使用上和ArrayBlockingQueue基本一样,采用FIFO,插入队尾,取出队头。
链表实际无容量限制,但有定义一个capacity,用来限制大小,在无参构造中,capacity默认int最大值,而在有参时可以指定,当指定capacity后,达到capacity时插入元素会阻塞。
构造器:
LinkedBlockingQueue<E>();
LinkedBlockingQueue<E>(int capacity);
常用方法:
put(E e);//阻塞放入元素
E take();//阻塞取出元素
3.SynchronousQueue
无缓冲区的阻塞队列,每个插入操作必须等待另一个线程的对应取出操作,反之取出也要等插入。(等于是一开始的耦合实现,只是还是添加的中间的中介,让生产者和消费者的代码没有直接耦合在一起。)
在Executors.newCachedThreadPool()构造的线程池中,阻塞队列参数即为SynchronousQueue(),那么多个线程就相当于多个生产者,线程池的sumbit,相当于生产者提交线程,然后经过阻塞队列,不进行存储,直接提交到消费者处,进行中线程处,开始运行。
构造器:
SynchronousQueue<E>();
SynchronousQueue<E>(int capacity);
常用方法:
put(E e);//阻塞放入元素
E take();//阻塞取出元素
4.DelayQueue<E extends Delayed>
无界阻塞队列,用作延迟队列,可以添加带有延迟时间的任务。
内部通过优先级队列PriorityQueue实现,并且添加的元素必须实现Delayed接口。
Delayed接口说明:
public interface Delayed
{
long getDelay(TimeUnit unit);
}
class DelayTask implements Delayed
{
private long delay; //延迟时间
private long expire; //预计到达时间
DelayTask(long delay)
{
this.delay = delay;
expire = System.currentTimeMillis() + delay;
}
//距离预计时间还剩多少时间
long getDelay(TimeUnit unit)
{
return (long)(expire - System.currentTimeMillis());
}
public int compareTo(Delayed o)
{
return (int) (this.getDelay(TimeUnit.MILLISECONDS) -o.getDelay(TimeUnit.MILLISECONDS));
}
}
重写compareTo()为了在优先级队列中实现排序。
队列添加实现了Delayed接口的元素,然后put()与take()会按照元素本身设定的delay延时,进行相应操作。
5.PriorityBlockingQueue
……