阻塞队列
基本概念
什么是队列?
看下面的图理解理解:
当然,我作为专业的程序员,在我眼里,上面的图就是这样的:
什么是阻塞队列呢?
大概是这样的:
你坐过公交车吗? 不对应该是挤过公交吗?就是那种只有后面下去一个,前面才能上去一个。
大概是这样的:
嗯!如果你挤过这样的公交,那就是讲过阻塞队列了。…
队列就是我们常说的先进先出的线性数据结构。
而阻塞队列 是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
java中的阻塞队列
JDK7提供了7个阻塞队列。分别是
ArrayBlockingQueue : 一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue : 一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue : 一个支持优先级排序的无界阻塞队列。
DelayQueue: 一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue: 一个不存储元素的阻塞队列。
LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。
BlockingQueue接口 与 BlockingDeque 接口
JDK提供的阻塞队列中,LinkedBlockingDeque 是一个 Deque(双向的队列),其实现的接口是 BlockingDeque;其余6个阻塞队列则是 Queue(单向队列),实现的接口是 BlockingQueue。
对于 BlockingQueue 的阻塞队列提供了四种处理方法:
- 抛出异常: 是指当阻塞队列满时候,再往队列里插入元素,会抛出IllegalStateException(“Queue full”)异常。当队列为空时,从队列里获取元素时会抛出NoSuchElementEx·ception异常 。
- 返回特殊值: 插入方法会返回是否成功,成功则返回true。移除方法,则是从队列里拿出一个元素,如果没有则返回null
一直阻塞: 当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到拿到数据,或者响应中断退出。当队列空时,消费者线程试图从队列里take元素,队列也会阻塞消费者线程,直到队列可用。 - 超时退出: 当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出。
抛出异常 与 返回特殊值 方法的实现是一样的,只不过对失败的操作的处理不一样!通过 AbstractQueue 的源码可以发现,add(e),remove(),element() 都是分别基于 offer(),poll(),peek() 实现的
public boolean add(E arg0) {
if (this.offer(arg0)) {
return true;
} else {
throw new IllegalStateException("Queue full");
}
}
public E remove() {
Object arg0 = this.poll();
if (arg0 != null) {
return arg0;
} else {
throw new NoSuchElementException();
}
}
public E element() {
Object arg0 = this.peek();
if (arg0 != null) {
return arg0;
} else {
throw new NoSuchElementException();
}
}
-
BlockingQueue不接受null元素。 实现抛出NullPointerException上尝试add , put或offer一个null 。 A null用作哨兵值以指示poll操作失败。
-
BlockingQueue可能是容量有限的。 在任何给定的时间它可能有一个remainingCapacity超过其中没有额外的元素可以put没有阻止。 没有任何内在容量限制的A BlockingQueue总是报告剩余容量为Integer.MAX_VALUE 。
-
BlockingQueue实现被设计为主要用于生产者 - 消费者队列,但另外支持Collection接口。 因此,例如,可以使用remove(x)从队列中删除任意元素。 然而,这样的操作通常不能非常有效地执行,并且仅用于偶尔使用,例如当排队的消息被取消时。
-
BlockingQueue实现是线程安全的。 所有排队方法使用内部锁或其他形式的并发控制在原子上实现其效果。 然而, 大量的Collection操作addAll , containsAll , retainAll和removeAll 不一定原子除非在实现中另有规定执行。 因此有可能,例如,为addAll©到只增加一些元件在后失败(抛出异常) c 。
-
BlockingQueue上不支持任何类型的“关闭”或“关闭”操作,表示不再添加项目。 这些功能的需求和使用往往依赖于实现。 例如,一个常见的策略是生产者插入特殊的尾流或毒物 ,这些消费者在被消费者摄取时被相应地解释。
使用示例,基于典型的生产者 - 消费者场景。 请注意, BlockingQueue可以安全地与多个生产者和多个消费者一起使用。
class Producer implements Runnable {
private final BlockingQueue queue;
Producer(BlockingQueue q) {
queue = q;
}
public void run() {
try {
while (true) {
queue.put(produce());
}
} catch (InterruptedException ex) {
... handle ...
}
}
Object produce() {
...
}
}
class Consumer implements Runnable {
private final BlockingQueue queue;
Consumer(BlockingQueue q) {
queue = q;
}
public void run() {
try {
while (true) {
consume(queue.take());
}
} catch (InterruptedException ex) { ... handle ...}
}
void consume(Object x) { ... }
}
class Setup {
void main() {
BlockingQueue q = new SomeQueueImplementation();
Producer p = new Producer(q);
Consumer c1 = new Consumer(q);
Consumer c2 = new Consumer(q);
new Thread(p).start();
new Thread(c1).start();
new Thread(c2).start();
}
}
存储器一致性效果:当与其他并发集合,事先将物体放置成在一个线程动作BlockingQueue happen-before到该元素的从访问或移除后续动作BlockingQueue在另一个线程。
七个阻塞队列的详细介绍
1.ArrayBlockingQueue
ArrayBlockingQueue是一个用数组实现的 有界阻塞队列。 此队列按照先进先出(FIFO)的原则对元素进行排序。
默认情况下不保证访问者公平地访问队列 ,所谓公平访问队列是指阻塞的线程,可按照阻塞的先后顺序访问队列。非公平性是对先等待的线程是不公平的,当队列可用时,阻塞的线程都可以竞争访问队列的资格。
为了保证公平性,通常会降低吞吐量。
ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);**加粗样式**
访问者的公平性是使用可重入锁实现的 ,代码如下:
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
案例:
public class ArrayBlockingQueueTest {
public static void main(String[] args) throws InterruptedException {
//创建阻塞队列
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>( 3 );
// 获取元素
//移除第一个值
//String obj = blockingQueue.remove();//出异常
//String obj = blockingQueue.poll();// 返回null;
//System.out.println(obj);
//String obj = blockingQueue.take();// 阻塞
//String obj = blockingQueue.poll(5, TimeUnit.SECONDS);// 超时退出,返回null
//System.out.println(obj);
// 检查值
//System.out.println(blockingQueue.element());//出异常
//System.out.println(blockingQueue.peek());// 返回null
// 添加三个值
System.out.println( blockingQueue.add( "a" ) );
System.out.println( blockingQueue.add( "b" ) );
System.out.println( blockingQueue.add( "c" ) );
// 添加值
//blockingQueue.add("d");//出异常
//boolean d = blockingQueue.offer("d");// 返回false
//blockingQueue.put("d");// 阻塞
boolean d = blockingQueue.offer("d", 3, TimeUnit.SECONDS);// 超时退出
System.out.println(d);
}
}
2. LinkedBlockingQueue
LinkedBlockingQueue是一个用链表实现的 有界阻塞队列。此队列的默认和最大长度为Integer.MAX_VALUE。 此队列按照先进先出的原则对元素进行排序。
public class LinkedBlockingQueueTest {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>(3);
// 获取元素
//移除第一个值
//String obj = blockingQueue.remove();//出异常
//String obj = blockingQueue.poll();// 返回null;
//System.out.println(obj);
//String obj = blockingQueue.take();// 阻塞
//String obj = blockingQueue.poll(5, TimeUnit.SECONDS);// 超时退出,返回null
//System.out.println(obj);
// 检查值
//System.out.println(blockingQueue.element());//出异常
//System.out.println(blockingQueue.peek());// 返回null
// 添加三个值
System.out.println( blockingQueue.add( "a" ) );
System.out.println( blockingQueue.add( "b" ) );
System.out.println( blockingQueue.add( "c" ) );
// 添加值
//blockingQueue.add("d");//出异常
//boolean d = blockingQueue.offer("d");// 返回false
//blockingQueue.put("d");// 阻塞
//boolean d = blockingQueue.offer("d", 3, TimeUnit.SECONDS);// 超时退出
//System.out.println(d);
}
}
3. PriorityBlockingQueue
PriorityBlockingQueue是一个支持优先级的无界队列(虽然此队列逻辑上是无界的,但是资源被耗尽时试图执行 add 操作也将失败,导致 OutOfMemoryError)。默认情况下元素采取自然顺序排列(每个元素都必须实现 Comparable 接口),也可以通过比较器comparator来指定元素的排序规则。元素按照升序排列.
其iterator() 方法中提供的迭代器并不 保证以特定的顺序遍历 PriorityBlockingQueue 的元素。如果需要 有序地进行遍历, 则应考虑使用 Arrays.sort(pq.toArray())。此外,可以使用方法 drainTo 按优先级顺序移除 全部或部分元素,并将它们放在另一个 collection 中。
public class PriorityBlockingQueueTest {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> blockingQueue = new PriorityBlockingQueue<>(3);
// 获取元素
//移除第一个值
//String obj = blockingQueue.remove();//出异常
//String obj = blockingQueue.poll();// 返回null;
//System.out.println(obj);
//String obj = blockingQueue.take();// 阻塞
//String obj = blockingQueue.poll(5, TimeUnit.SECONDS);// 超时退出,返回null
//System.out.println(obj);
// 检查值
//System.out.println(blockingQueue.element());//出异常
//System.out.println(blockingQueue.peek());// 返回null
// 添加三个值
System.out.println( blockingQueue.add( "a" ) );
System.out.println( blockingQueue.add( "c" ) );
System.out.println( blockingQueue.add( "b" ) );
// 添加值
//blockingQueue.add("d");//出异常
//boolean d = blockingQueue.offer("d");// 返回false
//blockingQueue.put("d");// 阻塞
//boolean d = blockingQueue.offer("d", 3, TimeUnit.SECONDS);// 超时退出
//System.out.println(d);
// 获取3次,看顺序
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
}
}
4. DelayQueue
Delayed 元素的一个无界阻塞队列,只有在延迟期满时才能从中提取元素。注意 DelayQueue 的所有方法只能操作“到期的元素“,例如,poll()、remove()、size()等方法,都会忽略掉未到期的元素。
我们可以将DelayQueue运用在以下应用场景:
缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,比如TimerQueue就是使用DelayQueue实现的。
DelayQueue 的实现是基于 PriorityQueue,是一个优先级队列,是以延时时间的长短进行排序的。所以,DelayQueue 需要知道每个元素的延时时间,而这个延时时间是由 Delayed 接口的 getDelay()方法获取的。所以, DelayQueue 的元素必须实现 Delay 接口;
//计算并返回延时时间
public long getDelay(TimeUnit unit) {
return unit.convert(time - now(), TimeUnit.NANOSECONDS);
}
延时队列的原理
延时队列的实现很简单,当消费者从队列里获取元素时,如果元素没有达到延时时间,就阻塞当前线程。
long delay = first.getDelay(TimeUnit.NANOSECONDS);
if (delay <= 0)
return q.poll();
else if (leader != null)
available.await();
案例:
public class DelayQueueTest {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Element> queue = new DelayQueue<>();
queue.add(new Element(10,"张三",TimeUnit.SECONDS));
queue.add(new Element(5,"李四",TimeUnit.SECONDS));
queue.add(new Element(15,"王五",TimeUnit.SECONDS));
//获取值
System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take());
}
}
class Element implements Delayed {
//触发时间
private long time;
private String value;// 信息
public Element(long time, String value,TimeUnit unit) {
this.time = System.currentTimeMillis() + (time > 0?unit.toMillis(time):0);
this.value = value;
}
@Override
public long getDelay(TimeUnit unit) {
return time - System.currentTimeMillis();
}
@Override
public int compareTo(Delayed o) {
Element e = (Element) o;
long deff = this.time - e.time;
if(deff <=0 ){
return -1;
}else {
return 1;
}
}
public String toString(){
return value;
}
}
5. SynchronousQueue
一种阻塞队列,其中每个插入操作必须等待另一个线程的对应移除操作 ,反之亦然。
SynchronousQueue 的几个特点
- 同步队列没有任何内部容量,甚至连一个队列的容量都没有。 所以很多继承的方法就没有用了,(如 isEmpty()始终返回true,size()为0,包含contain、移除remove 都始终为false 等等)。或者说,真正有意义的只有以下几个方法:获取并移除(poll()、poll(timeout,timeunit)、take())、插入(offer()、offer(timeout,timeunit)、put());
- 适合于传递性设计,在这种设计中, 每一个put操作必须等待一个take操作,反之亦然 。(当然,如果用的是offer、poll的话,那么就不会阻塞等待)。SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。
- 支持可选的公平排序策略。 默认情况下不保证这种排序。但是,使用公平设置为 true 所构造的队列可保证线程以 FIFO 的顺序进行访问。
//设置公平性的构造方法
public SynchronousQueue(boolean fair)
创建一个具有指定公平策略的 SynchronousQueue。
案例:
public class SynchronousQueueTest {
public static void main(String[] args) {
BlockingQueue<String> queue = new SynchronousQueue<>();
new Thread(()->{
// 生成线程启动
System.out.println("生成线程启动");
try {
Thread.sleep(5000);
queue.offer("卡卡西");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
// 消费线程启动
System.out.println("消费线程启动");
try {
System.out.println(queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
6. LinkedTransferQueue
LinkedTransferQueue是一个由链表结构组成的 无界阻塞TransferQueue队列 。相对于其他阻塞队列LinkedTransferQueue多了tryTransfer和transfer方法。
transfer方法: 如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回。transfer方法的关键代码如下:
Node pred = tryAppend(s, haveData);
return awaitMatch(s, pred, e, (how == TIMED), nanos);
tryTransfer方法: 则是用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回。而transfer方法是必须等到消费者消费了才返回。
对于带有时间限制的 tryTransfer(E e, long timeout, TimeUnit unit)方法 ,则是试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回false,如果在超时时间内消费了元素,则返回true。
LinkedTransferQueue采用的一种预占模式。意思就是消费者线程取元素时,如果队列为空,那就生成一个节点(节点元素为null)入队,然后消费者线程park住,后面生产者线程入队时发现有一个元素为null的节点,生产者线程就不入队了,直接就将元素填充到该节点,唤醒该节点上park住线程,被唤醒的消费者线程拿货走人。
7. LinkedBlockingDeque
LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列。所谓双向队列指的你可以从队列的两端插入和移出元素。 双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。 相比其他的阻塞队列,LinkedBlockingDeque多了addFirst,addLast,offerFirst,offerLast,peekFirst,peekLast等方法。另外,插入方法add等同于addLast,移除方法remove等效于removeFirst。但是take方法却等同于takeFirst,不知道是不是Jdk的bug,使用时还是用带有First和Last后缀的方法更清楚。
和 LinkedBlockingQueue 一样,是有界的阻塞队列,默认长度以及最大长度是 Integer.MAX_VALUE。可在创建时,指定容量。