BlockingQueue 简述
- 阻塞队列 (BlockingQueue)是Java.util.concurrent包下重要的数据结构,经常与CountDownLatch、CyclicBarrier、Semaphore这些一起谈论,可以参考《CountDownLatch 与 CyclicBarrier》
- BlockingQueue 提供了线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。
- 并发包下很多高级同步类的实现都是基于BlockingQueue实现的。
- public interface BlockingQueue<E>extends Queue<E>
- BlockingQueue 不接受 null 元素。试图 add、put 或 offer 一个 null 元素时,某些实现会抛出 NullPointerException。null 被用作指示 poll 操作失败的警戒值。
- BlockingQueue 可以是限定容量的,没有容量约束时,如LinkedBlockingDeque<E>、LinkedBlockingQueue<E>默认 大小为Integer.MAX_VALUE
- BlockingQueue 实现是线程安全的。
- BlockingQueue典型应用就是生产者-消费者使用场景,BlockingQueue 可以安全地与多个生产者和多个使用者一起使用。
四种操作形式
- BlockingQueue 的方法以四种形式出现,对于不能立即满足但可能在将来某一时刻可以满足的操作,这四种形式的处理方式不同,如下表所示:
操作 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
插入 | add(e) |
offer(e) |
put(e) |
offer(e, time, unit) |
移除 | remove() |
poll() |
take() |
poll(time, unit) |
检查 | element() |
peek() |
不可用 | 不可用 |
- 第一种:如果试图的操作无法立即执行,抛出异常
- 第二种:如果试图的操作无法立即执行,返回一个特定的值(true / false /null取决于具体操作)。
- 第三种:如果试图的操作无法立即执行,该方法调用将会无限期阻塞,直到条件满足后再继续执行
- 第四种:如果试图的操作无法立即执行,该方法调用将会在指定时间内阻塞,直到条件满足后再继续执行或者超时后抛出异常
BlockingQueue实现类
- BlockingQueue是接口,需要依靠它的实现BlockingQueue使用,主要实现类如下。
ArrayBlockingQueue
- public class ArrayBlockingQueue<E>extends AbstractQueue<E>implements BlockingQueue<E>, Serializable
- 一个由数组支持的有界阻塞队列,可以在对其初始化的时候设定这个有界上限,设定之后无法修改。
- 数组阻塞队列按 FIFO(先进先出)原则对元素进行排序。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。
DelayQueue
- public class DelayQueue<E extends Delayed>extends AbstractQueue<E>implements BlockingQueue<E>
- 延迟阻塞队列(DelayQueue)对元素进行持有直到一个特定的延迟到期。注入其中的元素必须实现 java.util.concurrent.Delayed 接口。
- 实际使用的少
LinkedBlockingQueue
- public class LinkedBlockingQueue<E>extends AbstractQueue<E>implements BlockingQueue<E>, Serializable
- 一个基于已链接节点的、范围任意的链式阻塞队列
- 此队列按 FIFO(先进先出)排序元素,新元素插入到队列的尾部,并且队列获取操作会获得位于队列头部的元素
- 链接队列的吞吐量通常要高于基于数组的队列,但是在大多数并发应用程序中,其可预知的性能要低。
- 可选的容量范围构造方法参数作为防止队列过度扩展的一种方法。如果未指定容量,则它等于 Integer.MAX_VALUE。如果已经指定容量,则容量不会再动态改变
PriorityBlockingQueue
- public class PriorityBlockingQueue<E>extends AbstractQueue<E>implements BlockingQueue<E>, Serializable
- PriorityBlockingQueue 是一个无界阻塞队列。它使用了和类 java.util.PriorityQueue 一样的排序规则。
- 无法向这个队列中插入 null 值。
SynchronousQueue
- public class SynchronousQueue<E>extends AbstractQueue<E>implements BlockingQueue<E>, Serializable
- SynchronousQueue 同步阻塞队列,每次插入操作时必须同时有另一个线程在做对应的移除操作 ,否则插入操作会一直阻塞;反之,做移除操作时,必须有另一个线程在做插入操作,否则移除操作会一直阻塞。这样达到完全同步的效果!
- 同步队列没有任何内部容量,是一个空 collection,所以插入与移除操作只能同时进行,否则只做插入或者只做移除时,此操作就会一直阻塞。
- 此队列不允许 null 元素
- 同步队列非常适合于传递性设计,在这种设计中,在一个线程中运行的对象要将某些信息、事件或任务传递给在另一个线程中运行的对象,它就必须与该对象同步。
生产者-消费者
/** * Created by Administrator on 2018/6/14 0014. * 生产者 */ public class Producer extends Thread { /** * blockingQueue:阻塞队列 * random:用于产生随机整数 */ private BlockingQueue<String> blockingQueue; private Random random; public Producer(BlockingQueue blockingQueue) { this.blockingQueue = blockingQueue; this.random = new Random(); } @Override public void run() { try { System.out.println(Thread.currentThread().getName() + " 生产者生产开始... " + new Date()); /** * 循环随机产生3个字符串*/ for (int i = 0; i < 3; i++) { TimeUnit.SECONDS.sleep(1 + random.nextInt(5)); String info = "中国制造" + random.nextInt(100); /**往链式队列中添加元素,如果队列中的元素个数以已经达到队列容量,则put方法会阻塞 * 直到里面有元素被移除时,才能再次添加 * 链式队列添加元素与消费者是否已经消费无关,只要队列有容量,则可以一直添加*/ blockingQueue.put(info); System.out.println(Thread.currentThread().getName() + " 生产了 " + info + " " + new Date()); } System.out.println(Thread.currentThread().getName() + " 生产者生产结束,通知消费者结束... " + new Date()); /**这里往队列中插入一个标识"exit",告诉它生产已经完毕,本任务即将结束*/ blockingQueue.put("exit"); } catch (InterruptedException e) { e.printStackTrace(); } } }
/** * Created by Administrator on 2018/6/14 0014. * 消费者 */ public class Consumer extends Thread { /** * blockingQueue:阻塞队列 * countDownLatch:倒计数锁存器,用于将来告诉主线程消费者已经消费结束了 * isExit:在收到生产者结束标识"exit"消息之前,消费者会一直循环阻塞的获取消息 * 当isExit为true时,退出循环,消费任务结束 * random:用于生产随机数 */ private BlockingQueue<String> blockingQueue; private CountDownLatch countDownLatch; public static boolean isExit = false; private Random random; /** * 初始化参数值 * * @param blockingQueue * @param countDownLatch */ public Consumer(BlockingQueue blockingQueue, CountDownLatch countDownLatch) { this.blockingQueue = blockingQueue; this.countDownLatch = countDownLatch; random = new Random(); } @Override public void run() { try { System.out.println(Thread.currentThread().getName() + " 消费者开始消费... " + new Date()); while (!isExit) { /**获取链式队列中头部元素,FIFO(先进先出) * 队列中没有元素时,take会一直阻塞,获取到之后,队列原位置的元素就会被删除*/ String info = blockingQueue.take(); System.out.println(Thread.currentThread().getName() + " 消费了 " + info + " " + new Date()); TimeUnit.SECONDS.sleep(1 + random.nextInt(5)); /**如果收到"exit"消息,则退出循环*/ if ("exit".equals(info)) { isExit = true; } } System.out.println(Thread.currentThread().getName() + " 消费者结束消费... " + new Date()); /**倒计数锁存器计数减一*/ countDownLatch.countDown(); } catch (InterruptedException e) { e.printStackTrace(); } } }
/** * Created by Administrator on 2018/6/14 0014. * 测试类 */ public class Test { public static void main(String[] args) { try { /**创建链式阻塞队列,并指定队列容量为10,注意当队列中元素个数达到10个时,使用put方法再次添加时则会阻塞 * 普通的List即使设置了初始大小,超过容量时还是会自动改变容量;LinkedBlockingQueue一旦指定了就不会改变容量了 * */ BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>(10); /**因为会让主线程等待消费者线程,所以倒计数锁存器同步个数要与消费者个数一致*/ CountDownLatch countDownLatch = new CountDownLatch(1); /**采用线程池方式 * */ ExecutorService executorService = Executors.newCachedThreadPool(); /** 阻塞队列是线程安全的,无论多少生产和消费者都是可行的,注意点如下: * 一个生产者、一个消费者时:生产者的添加的元素都会被同一个消费者获取 * 一个生产者、多个消费者时:生产者的添加的元素会被多个消费者获取,A获取了,则B、C...其它所有的就没有了 * 多个生产者、多个消费者时:所有生产者的添加的元素会放入同一个队列,而多个消费者则从队列中获取- * 队列中的元素被哪个消费者获取了之后,其余消费者就没有了 */ for (int i = 0; i < 2; i++) { if (i < 1) { executorService.execute(new Producer(blockingQueue)); } else { executorService.execute(new Consumer(blockingQueue, countDownLatch)); } } /**主线程开始阻塞,等待消费者消费完成*/ countDownLatch.await(); System.out.println("主线程开始关闭线程池..."); executorService.shutdown(); } catch (InterruptedException e) { e.printStackTrace(); } } }
------------------------结果输出-------------------------
pool-1-thread-2 消费者开始消费... Fri Jun 15 10:14:52 CST 2018
pool-1-thread-1 生产者生产开始... Fri Jun 15 10:14:52 CST 2018
pool-1-thread-1 生产了 中国制造5 Fri Jun 15 10:14:57 CST 2018
pool-1-thread-2 消费了 中国制造5 Fri Jun 15 10:14:57 CST 2018
pool-1-thread-1 生产了 中国制造79 Fri Jun 15 10:15:00 CST 2018
pool-1-thread-2 消费了 中国制造79 Fri Jun 15 10:15:01 CST 2018
pool-1-thread-1 生产了 中国制造36 Fri Jun 15 10:15:05 CST 2018
pool-1-thread-1 生产者生产结束,通知消费者结束... Fri Jun 15 10:15:05 CST 2018
pool-1-thread-2 消费了 中国制造36 Fri Jun 15 10:15:06 CST 2018
pool-1-thread-2 消费了 exit Fri Jun 15 10:15:08 CST 2018
pool-1-thread-2 消费者结束消费... Fri Jun 15 10:15:13 CST 2018
主线程开始关闭线程池...
Process finished with exit code 0