一、BlockingQueue概述

1、阻塞的含义

BlockingQueue即阻塞队列,从阻塞这个词可以看出,在某些情况下对阻塞队列的访问可能会造成阻塞。被阻塞的情况主要有如下两种:

  • ,当一个线程对已经满了的阻塞队列进行入队操作时会阻塞,即线程会挂起直到队列不满时,线程才继续入队
  • 当一个线程对一个空的阻塞队列进行出队操作时也会阻塞,即线程会挂起直到队列不空,线程才继续出队

2、为什么要使用BlockingQueue

多线程环境中,通过队列可以很容易实现数据共享,比如经典的“生产者”和“消费者”模型中,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然。然而,在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。好在此时,强大的concurrent包横空出世了,而他也给我们带来了强大的BlockingQueue。(在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒)

3、BlockingQueue的一组方法

1)放入数据

    offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.(本方法不阻塞当前执行方法的线程);      

    offer(E o, long timeout, TimeUnit unit):可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。

    put(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续。

              add(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则抛出异常。

2). 获取数据

    poll(time): 取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null;

    poll(long timeout, TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则知道时间超时还没有数据可取,返回失败。

    take(): 取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入;

    drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。

以下重点讲三个队列,都实现了BlockingQueue

二、DelayQueue

1、使用场景

  常常会遇到一些延迟任务(100ms后执行该任务)、周期任务(每10ms执行一次)、超时任务(比如缓存,超时就要移除)等。如果我们要创建一个处理这样任务的调度服务,那么DelayedQueue将是首选!

2、DelayQueue和Delayed简介

DelayedQueue是一个无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的Delayed元素。如果延迟都还没有期满,则队列没有头部,并且poll将返回null。当一个元素的 getDelay(TimeUnit.NANOSECONDS)方法返回一个小于等于0的值时,将发生到期。即使无法使用take或poll移除未到期的元素,也不会将这些元素作为正常元素对待。例如,size方法同时返回到期和未到期元素的计数。此队列不允许使用null元素。

因此DelayQueue可以理解为一个使用时间作为比较条件的PriorityBlockingQueue(优先级阻塞队列)。

priorityQueue是一种优先级队列,这里优先级就是延迟时间,也就是说进入队列的任务安装优先级进行排序,延迟时间最短的在队列前面,先被处理,也就是说,每次从队列中取出的任务都将是到期的任务

  DelayQueue里面的元素必须是实现了Delayed接口的元素,Delayed接口使对象成为延迟对象,它使存放在DelayQueue类中的对象具有了激活日期。该接口强制执行下列两个方法,也就是说不需要程序去显示调用。

  • CompareTo(Delayed o):Delayed接口继承了Comparable接口,进入队列里面的元素会被队列自动用此方法比较两个元素的延迟时间进行排序
  • getDelay(TimeUnit unit):这个方法返回到激活日期的剩余时间,时间单位由单位参数指定。

3、示例 

夏天买来一批食品放入冷藏室,每种食品在冷藏室都有一个保存时间,超过该时间就会变质。食品检查员经常检查食品,超过冷藏时间的食品就要拿出来扔掉。

1)定义一个食品类:Food

  1. class Food implements Delayed{
  2. private String foodName;
  3. private long saveTime;//保存时间
  4. private long expireTime;//过期时刻=当前时间+保存时间
  5. public Food(String foodName,long saveTime){
  6. this.foodName=foodName;
  7. this.saveTime=saveTime;
  8. this.expireTime=TimeUnit.NANOSECONDS.convert(saveTime, TimeUnit.SECONDS)+System.nanoTime();
  9. }
  10. @Override
  11. public int compareTo(Delayed o) {
  12. Food that = (Food)o; //that指的是同一队列里面的其他元素。
  13. if(this.expireTime > that.expireTime){//过期时刻越靠后,越排在队尾.
  14. return 1;
  15. }else if(this.expireTime==that.expireTime){
  16. return 0;
  17. }else{
  18. return -1;
  19. }
  20. }
  21. @Override
  22. public long getDelay(TimeUnit unit) {
  23. return unit.convert(this.expireTime-System.nanoTime(), TimeUnit.NANOSECONDS);
  24. }
  25. public String getFoodName(){
  26. return this.foodName;
  27. }
  28. public long getExpireTime(){
  29. return this.expireTime;
  30. }
  31. public long getSaveTime(){
  32. return this.saveTime;
  33. }
  34. }

这里,食品类实现了Delayed接口,因此重写了compareTo和getDelay方法。getDelay返回与此对象相关的剩余延迟时间,以给定的时间单位表示。

下面定义一个食品检查员的类:

2)FoodChecker

  1. class FoodChecker implements Runnable{
  2. private DelayQueue<Food> queue;
  3. public FoodChecker(DelayQueue<Food> queue){
  4. this.queue=queue;
  5. }
  6. @Override
  7. public void run() {
  8. try{
  9. System.out.println("开始检查!");
  10. boolean flag = true;
  11. while(flag){
  12. Food food = queue.take();//此处会阻塞,没有时过期食品时不会取出
  13. System.out.println(food.getFoodName()+"食品过期!保存时间:"+food.getSaveTime()+"天.");
  14. if(queue.isEmpty()){
  15. flag=false;
  16. }
  17. }
  18. System.out.println("检查完毕!");
  19. }catch(Exception e){
  20. e.printStackTrace();
  21. }
  22. }
  23. }

食品检查员持有一个DelayQueue,并且不断从该队列中取出过期食品处理掉。

3)主任务类

  1. DelayQueue<Food> queue=new DelayQueue<Food>();
  2. Random r = new Random();
  3. queue.add(new Food("A", getRandomDay(r)));
  4. queue.add(new Food("B", getRandomDay(r)));
  5. queue.add(new Food("C", getRandomDay(r)));
  6. queue.add(new Food("D", getRandomDay(r)));
  7. queue.add(new Food("E", getRandomDay(r)));
  8. queue.add(new Food("F", getRandomDay(r)));
  9.  
  10. ExecutorService es = Executors.newSingleThreadExecutor();
  11. es.execute(new FoodChecker(queue));
  12. es.shutdown();

在队列中放入几种食品,并随机给一个保存时间。由于FoodChecker只有一个线程,所以这里使用的是SingleThreadExecutor。

运行结果如下:

  1. 开始检查!
  2. B食品过期!保存时间:1天.
  3. C食品过期!保存时间:2天.
  4. A食品过期!保存时间:5天.
  5. D食品过期!保存时间:5天.
  6. E食品过期!保存时间:6天.
  7. F食品过期!保存时间:10天.
  8. 检查完毕!

这个例子中使用的是DelayQueue的take方法,该方法获取并移除此队列的头部,在可从此队列获得延迟到期的元素之前会一直等待(即阻塞)。另外,还有几个常用方法如下:

poll():获取并移除此队列的头,如果此队列不包含具有已到期延迟时间的元素,则返回 null。(非阻塞)

peek():获取但不移除此队列的头部;如果此队列为空,则返回 null。与poll不同,如果队列中没有到期元素可用,则此方法返回下一个将到期的元素(如果存在一个这样的元素)。

三、ArrayBlockingQueue和LinkedBlockingQueue

1、使用场景 :主要用于实现“生产者-消费者”模式

2、特点

  1)ArrayBlockingQueue

  • 内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,takeIndex和putIndex来维护队列头和尾部的游标
  • ArrayBlockingQueue 的生产和消费使用同一个ReentranLock来同步,使用Condition的方法来同步和通信:await()和signal(),这导致生产和消费不能同时进行
  • ArrayBlockingQueue 初始化必须指定容量大小

  • ArrayBlockingQueue 进行插入和删除时,直接将对象插入或移除,不会产生或销毁任何额外的对象实例

2)LinkedBlockingQueue

  • 内部也维持着一个数据缓冲队列(该队列由一个链表构成),无边界的堵塞队列
  • LinkedBlockingQueue的消费和生产 使用 读写2个锁来对写和读进行加锁操作,这样相比一个锁的好处是细化了锁的跨度,读写分离,减小了锁的竞争
  • LinkedBlockingQueue 有默认的容量大小为:Integer.MAX_VALUE,当然也可以传入指定的容量大小(非必须指定),而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了
  • LinkedBlockingQueue 在生产和消费的时候,需要创建Node对象进行插入或移除,大批量数据的系统中,其对于GC的压力会比较大

3、示例程序

1)生产者:

  1. package blockingqueue.demo;
  2.  
  3. import java.util.Random;
  4. import java.util.concurrent.ArrayBlockingQueue;
  5. import java.util.concurrent.BlockingQueue;
  6. import java.util.concurrent.LinkedBlockingQueue;
  7. import java.util.concurrent.TimeUnit;
  8. import java.util.concurrent.atomic.AtomicInteger;
  9.  
  10. /**
  11. * @author Administrator
  12. * @date 2018/12/26
  13. */
  14. public class FoodMaker implements Runnable {
  15.  
  16. private BlockingQueue blockingQueue;
  17. private String makerName;
  18. private Boolean runFlag = true;
  19. //这里为什么用AtomicInteger,而不用int i=0;i++这种。因为在高并发的情况下i++是线程不安全的,
  20. // 而AtomicInteger的incrementAndGet()方法在一个无限循环体内,不断尝试将一个比当前值大1的新值赋给自己,
  21. // 如果失败则说明在执行"获取-设置"操作的时已经被其它线程修改过了,于是便再次进入循环下一次操作,直到成功为止
  22. private static AtomicInteger foodCount = new AtomicInteger();
  23.  
  24. //构造函数
  25. public FoodMaker(BlockingQueue blockingQueue, String makerName) {
  26. this.blockingQueue = blockingQueue;
  27. this.makerName = makerName;
  28. }
  29.  
  30. public void run() {
  31. Random r = new Random();
  32. System.out.println("厨师:" + makerName + "开始做食物");
  33. try {
  34. while (runFlag) {
  35. //随机休眠一点时间,用随机的目的就是造成厨师制作食物的速度和吃食物的人的速度不匹配,以此来验证阻塞的作用
  36. Thread.sleep(r.nextInt(1000));
  37. String data = "food_" + foodCount.incrementAndGet();
  38. blockingQueue.put(data);//此处队列如果已经满了,当前线程会在这里挂起
  39. String msg = makerName + "将 " + data + " 放入了队列";
  40. System.out.println(msg);
  41. }
  42. } catch (InterruptedException e) {
  43. e.printStackTrace();
  44. Thread.currentThread().interrupt();
  45. } finally {
  46. System.out.println(makerName + "不再做食物!");
  47. }
  48. }
  49.  
  50. public void stop(){
  51. runFlag = false;
  52. }
  53.  
  54. }

2)消费者:

  1. package blockingqueue.demo;
  2.  
  3. import java.util.Random;
  4. import java.util.concurrent.BlockingQueue;
  5. import java.util.concurrent.atomic.AtomicInteger;
  6.  
  7. /**
  8. * @author Administrator
  9. * @date 2018/12/26
  10. */
  11. public class FoodEater implements Runnable {
  12.  
  13. private BlockingQueue blockingQueue;
  14. private String eaterName;
  15. private Boolean runFlag = true;
  16.  
  17. //构造函数
  18. public FoodEater(BlockingQueue blockingQueue, String eaterName) {
  19. this.blockingQueue = blockingQueue;
  20. this.eaterName = eaterName;
  21. }
  22.  
  23. public void run() {
  24. Random r = new Random();
  25. System.out.println("食者:" + eaterName + "走进了食堂");
  26. try {
  27. while (runFlag) {
  28. //随机休眠一点时间,用随机的目的就是造成厨师制作食物的速度和吃食物的人的速度不匹配,以此来验证阻塞的作用
  29. Thread.sleep(r.nextInt(1000));
  30. String msg1 = eaterName + "等待食物到来========";
  31. System.out.println(msg1);
  32. String data = blockingQueue.take().toString();
  33. String msg2 = eaterName + "吃了 " + data;
  34. System.out.println(msg2);
  35. }
  36. } catch (InterruptedException e) {
  37. e.printStackTrace();
  38. Thread.currentThread().interrupt();
  39. } finally {
  40. System.out.println(eaterName + "退出了食堂");
  41. }
  42. }
  43.  
  44. public void stop(){
  45. runFlag = false;
  46. }
  47.  
  48. }

3) 测试

  1. package blockingqueue.demo;
  2.  
  3. import java.util.concurrent.BlockingQueue;
  4. import java.util.concurrent.ExecutorService;
  5. import java.util.concurrent.Executors;
  6. import java.util.concurrent.LinkedBlockingQueue;
  7.  
  8. /**
  9. * @author Administrator
  10. * @date 2018/12/27
  11. */
  12. public class BlockingQueueTest {
  13. public static void main(String[] args) throws InterruptedException{
  14. BlockingQueue<String> queue = new LinkedBlockingQueue<String>(10);
  15. //三个生产者
  16. FoodMaker foodMaker1 = new FoodMaker(queue,"张三");
  17. FoodMaker foodMaker2 = new FoodMaker(queue,"李四");
  18. FoodMaker foodMaker3 = new FoodMaker(queue,"王五");
  19.  
  20. //两个消费者
  21. FoodEater foodEater1 = new FoodEater(queue,"马六");
  22. FoodEater foodEater2 = new FoodEater(queue,"赵七");
  23.  
  24. ExecutorService executorService = Executors.newCachedThreadPool();
  25. executorService.submit(foodMaker1);
  26. executorService.submit(foodMaker2);
  27. executorService.submit(foodMaker3);
  28. executorService.submit(foodEater1);
  29. executorService.submit(foodEater2);
  30.  
  31. Thread.sleep( 5000);
  32. foodMaker1.stop();
  33. foodMaker2.stop();
  34. foodMaker3.stop();
  35. Thread.sleep( 5000);
  36. foodEater1.stop();
  37. foodEater2.stop();
  38. Thread.sleep( 2000);
  39. executorService.shutdown();
  40. }
  41. }

执行结果:

  1. 厨师:王五开始做食物
  2. 食者:赵七走进了食堂
  3. 厨师:张三开始做食物
  4. 厨师:李四开始做食物
  5. 食者:马六走进了食堂
  6. 赵七等待食物到来========
  7. 王五将 food_1 放入了队列
  8. 赵七吃了 food_1
  9. 王五将 food_2 放入了队列
  10. 马六等待食物到来========
  11. 马六吃了 food_2
  12. 赵七等待食物到来========
  13. 李四将 food_3 放入了队列
  14. 赵七吃了 food_3
  15. 张三将 food_4 放入了队列
  16. 李四将 food_5 放入了队列
  17. 王五将 food_6 放入了队列
  18. 王五将 food_7 放入了队列
  19. 马六等待食物到来========
  20. 马六吃了 food_4
  21. 李四将 food_8 放入了队列
  22. 马六等待食物到来========
  23. 马六吃了 food_5
  24. 张三将 food_9 放入了队列
  25. 赵七等待食物到来========
  26. 赵七吃了 food_6
  27. 王五将 food_10 放入了队列
  28. 赵七等待食物到来========
  29. 赵七吃了 food_7
  30. 王五将 food_11 放入了队列
  31. 张三将 food_12 放入了队列
  32. 王五将 food_13 放入了队列
  33. 李四将 food_14 放入了队列
  34. 马六等待食物到来========
  35. 马六吃了 food_8
  36. 赵七等待食物到来========
  37. 赵七吃了 food_9
  38. 马六等待食物到来========
  39. 马六吃了 food_10
  40. 赵七等待食物到来========
  41. 赵七吃了 food_11
  42. 王五将 food_15 放入了队列
  43. 王五不再做食物!
  44. 马六等待食物到来========
  45. 马六吃了 food_12
  46. 张三将 food_16 放入了队列
  47. 张三不再做食物!
  48. 马六等待食物到来========
  49. 马六吃了 food_13
  50. 李四将 food_17 放入了队列
  51. 李四不再做食物!
  52. 马六等待食物到来========
  53. 马六吃了 food_14
  54. 赵七等待食物到来========
  55. 赵七吃了 food_15
  56. 马六等待食物到来========
  57. 马六吃了 food_16
  58. 赵七等待食物到来========
  59. 赵七吃了 food_17
  60. 马六等待食物到来========
  61. 赵七等待食物到来========

由以上结果看出来,食者线程并没有执行:System.out.println(eaterName + "退出了食堂");

这是因为当 blockingQueue.take() 的时候发现队列为空就阻塞在这里了,后面虽然调用了stop方法修改了runFlag=false,但是本次循环没有完,所以会一直卡在take的地方。

java Concurrent包学习笔记(四):BlockingQueue的更多相关文章

  1. java Concurrent包学习笔记(一):ExecutorService

    一.介绍 ExecutorService是java.util.concurrent包中的一个线程池实现接口.其有两个实现类: 1)ThreadPoolExecutor:普通线程池通过配置线程池大小,能 ...

  2. java Concurrent包学习笔记(三):ReentrantLock

    一.可重入性的理解 从名字上理解,ReenTrantLock的字面意思就是再进入的锁,其实synchronized关键字所使用的锁也是可重入的,两者关于这个的区别不大.两者都是同一个线程每进入一次,锁 ...

  3. java Concurrent包学习笔记(六):Exchanger

    一.概述 Exchanger 是一个用于线程间协作的工具类,Exchanger用于进行线程间的数据交换,它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据.这两个线程通过exchange 方法 ...

  4. java Concurrent包学习笔记(五):Semaphore

    一.Semaphore 是什么  信号量Semaphore是一个并发工具类,用来控制可同时并发的线程数,其内部维护了一组虚拟许可,构造函数初始化的时候可以指定许可的总数量 每次线程执行操作时先通过ac ...

  5. java Concurrent包学习笔记(二):CountDownLatch和CyclicBarrier

    一.CountDownLatch CountDownLatch一个线程同步的工具,是的一个或者多个线程等待其他线程操作完成之后再执行. CountDownLatch通过一个给定的数值count来进行初 ...

  6. java Concurrent包学习笔记(七):ConcurrentHashMap

    (注意:以下讲解的ConcurrentHashMap是jdk 1.8的) 一.ConcurrentHashMap的数据结构 ConcurrentHashMap在1.8中的实现,相比于1.7的版本基本上 ...

  7. java之jvm学习笔记四(安全管理器)

    java之jvm学习笔记四(安全管理器) 前面已经简述了java的安全模型的两个组成部分(类装载器,class文件校验器),接下来学习的是java安全模型的另外一个重要组成部分安全管理器. 安全管理器 ...

  8. java.util.concurrent包学习笔记(一)Executor框架

    类图: 其实从类图我们能发现concurrent包(除去java.util.concurrent.atomic 和 java.util.concurrent.locks)中的内容并没有特别多,大概分为 ...

  9. java package 包 学习笔记

    编译命令示例: javac -d . Main.java 注:带参数-d自动建立文件目录, 只使用javac 则需要手工创建目录 把 class文件打包 jar命令 jar cvf T.jar *; ...

随机推荐

  1. js中Number()、parseInt()和parseFloat()的区别

    一:Number() 如果是Boolean值,true和false值将分别被转换为1和0. 如果是数字值,只是简单的传入和返回. 如果是null值,返回0. 如果是undefined,返回NaN. 如 ...

  2. Dev使用技巧汇总

    C# XtraGrid的行指示器(RowIndicator)行号以及图标设置 参考网址:https://www.cnblogs.com/xuliangxing/p/6775438.html DateE ...

  3. 使用CCNode作为容器容易踩的坑

    Cocos2dx中CCNode经常作为一个父容器,里面装一些UI控件,最后组成一个复杂的自定义的UI控件,但是在使用别人的自定义控件和自己写自定义问题的时候会踩一些坑. 首先拿到一个自定义的UI控件一 ...

  4. GDI/GDI+这些破事

    本文是杂篇,纯属笔记,想到哪写到那! 1.获取像素的RGB以及填充 CPaintDC dc(m_hWnd); COLORREF color=dc.GetPixel(,); int R=GetRValu ...

  5. 深入浅出 Java Concurrency (8): 加锁的原理 (Lock.lock)

    接上篇,这篇从Lock.lock/unlock开始.特别说明在没有特殊情况下所有程序.API.文档都是基于JDK 6.0的. public void java.util.concurrent.lock ...

  6. day9-IO 番外

    同步IO和异步IO,阻塞IO和非阻塞IO分别是什么,到底有什么区别?不同的人在不同的上下文下给出的答案是不同的.所以先限定一下本文的上下文. 本文讨论的背景是Linux环境下的network IO. ...

  7. 一个不明觉厉的貌似包含很多linux资料索引的网页

    http://man.lupaworld.com/content/other/Linux/linuxmanage/node108.html 貌似是个官方的doc之类的...

  8. Ubuntu TIP

    recovery进系统硬盘是挂载为“只读”的,要想改文件需要remount / 并且添加“w”(写权限). 进一次crub,再root进入 折腾几次似乎就可以编辑磁盘上的文件了

  9. 重写iframe内联框架中的内容

    重写iframe内联框架中的内容,不使用src指向页面url,主动写入HTML代码: var ifr = document.getElementById("CMBC-certificatio ...

  10. JSTL中EL表达式无法直接取size的处理

    jsp中使用${list.size }会编译成list.getSize()方法,并不能获取list的长度,因为程序回去找List对象中的getSize()方法,所以只能想别的办法, 一种方法是在后台程 ...