• 在之前的线程池的介绍中我们看到了很多阻塞队列,这篇文章我们主要来说说阻塞队列的事。
  • 阻塞队列也就是 BlockingQueue ,这个类是一个接
  • 口,同时继承了 Queue 接口,这两个接口都是在JDK5 中加入的 。
  • BlockingQueue 阻塞队列是线程安全的,在我们业务中是会经常频繁使用到的,如典型的生产者消费的场景,生产者只需要向队列中添加,而消费者负责从队列中获取。

  • 如上图展示,我们生产者线程不断的put 元素到队列,而消费者从中take 出元素处理,这样实现了任务与执行任务类之间的解耦,任务都被放入到了阻塞队列中,这样生产者和消费者之间就不会直接相互访问实现了隔离提高了安全性。

并发队列

  • 上面是 Java 中队列Queue 类的类图,我们可以看到它分为两大类,阻塞队列与非阻塞队列
  • 阻塞队列的实现接口是 BlockingQueue 而非阻塞队列的接口是 ConcurrentLinkedQueue , 本文主要介绍阻塞队列,非阻塞队列不再过多阐述
  • BlockingQueue 主要有下面六个实现类,分别是 ArrayBlockingQueueLinkedBlockingQueueSynchronousQueueDelayQueuePriorityBlockingQueueLinkedTransferQueue 。这些阻塞队列有着各自的特点和适用场景,后面详细介绍。
  • 非阻塞队列的典型例子如 ConcurrentLinkedQueue , 它不会阻塞线程,而是利用了 CAS 来保证线程的安全。
  • 其实还有一个队列和 Queue 关系很紧密,那就是Deque,这其实是 double-ended-queue 的缩写,意思是双端队列。它的特点是从头部和尾部都能添加和删除元素,而我们常见的普通队列Queue 则是只能一端进一端出,即FIFO

阻塞队列特点

  • 阻塞队列的特点就在于阻塞,它可以阻塞线程,让生产者消费者得以平衡,阻塞队列中有两个关键方法 PutTake 方法

take方法

  • take 方法的功能是获取并移除队列的头结点,通常在队列里有数据的时候是可以正常移除的。可是一旦执行 take 方法的时候,队列里无数据,则阻塞,直到队列里有数据。一旦队列里有数据了,就会立刻解除阻塞状态,并且取到数据。过程如图所示:



put方法

  • put 方法插入元素时,如果队列没有满,那就和普通的插入一样是正常的插入,但是如果队列已满,那么就无法继续插入,则阻塞,直到队列里有了空闲空间。如果后续队列有了空闲空间,比如消费者消费了一个元素,那么此时队列就会解除阻塞状态,并把需要添加的数据添加到队列中。过程如图所示:



是否有界(容量有多大)

  • 此外,阻塞队列还有一个非常重要的属性,那就是容量的大小,分为有界和无界两种。
  • 无界队列意味着里面可以容纳非常多的元素,例如 LinkedBlockingQueue 的上限是 Integer.MAX_VALUE,约为 2 的 31 次方,是非常大的一个数,可以近似认为是无限容量,因为我们几乎无法把这个容量装满。
  • 但是有的阻塞队列是有界的,例如 ArrayBlockingQueue 如果容量满了,也不会扩容,所以一旦满了就无法再往里放数据了。

阻塞队列常见方法

  • 首先我们从常用的方法出发,根据各自的特点我们可以大致分为三个大类,如下表所示:
分类 方法 含义 特点
抛出异常 add 添加一个元素 如果队列已满,添加则抛出  IllegalStateException 异常
remove 删除队列头节点 当队列为空后,删除则抛出  NoSuchElementException 异常
element 获取队列头元素 当队列为空时,则抛出 NoSuchElementException 异常
返回无异常 offer 添加一个元素 当队列已满,不会报异常,返回  false ,如果成功返回 true
poll 获取队列头节点,并且删除它 当队列空时,返回  Null
peek 单纯获取头节点 当队列为空时反馈 NULL
阻塞 put 添加一个元素 如果队列已满则阻塞
take 返回并删除头元素 如果队列为空则阻塞
  • 如上面所示主要的八个方法,相对都比较简单,下面我们通过实际代码演示的方式来认识

抛异常类型[add、remove、element]

add

  • 向队列中添加一个元素。如果队列是有界队列,当队列已满时再添加则抛出异常提示,如下:
        BlockingQueue queue = new ArrayBlockingQueue(2);
queue.add(1);
queue.add(2);
queue.add(3);
  • 上述代码中我们创建了一个阻塞队列容量为2,当我们使用 add 向其中添加元素,当添加到第三个时则会抛出异常如下:

remove

  • remove 方法是从队列中删除队列的头节点,同时会返回该元素。当队列中为空时执行 remove 方法时则会抛出异常,代码如下:
    private static void groupRemove() {
BlockingQueue queue = new ArrayBlockingQueue(2);
queue.add("i-code.online");
System.out.println(queue.remove());
System.out.println(queue.remove());
}
  • 上述代码中,我们可以看到,我们想队列中添加了一个元素 i-code.online , 之后通过 remove 方法进行删除,当执行第二次remove 时队列内已无元素,则抛出异常。如下:

element

  • element 方法是获取队列的头元素,但是并不是删除该元素,这也是与 remove 的区别,当队列中没有元素后我们再执行 element 方法时则会抛出异常,代码如下:
    private static void groupElement() {
BlockingQueue queue = new ArrayBlockingQueue(2);
queue.add("i-code.online");
System.out.println(queue.element());
System.out.println(queue.element());
}
private static void groupElement2() {
BlockingQueue queue = new ArrayBlockingQueue(2);
System.out.println(queue.element());
}
  • 上面两个方法分别演示了在有元素和无元素的情况element 的使用。在第一个方法中并不会报错,因为首元素一直存在的,第二个方法中因为空的,所以抛出异常,如下结果:

无异常类型[offer、poll、peek]

offer

  • offer 方法是向队列中添加元素, 同时反馈成功与失败,如果失败则返回 false ,当队列已满时继续添加则会失败,代码如下:
    private static void groupOffer() {
BlockingQueue queue = new ArrayBlockingQueue(2);
System.out.println(queue.offer("i-code.online"));
System.out.println(queue.offer("云栖简码"));
System.out.println(queue.offer("AnonyStar"));
}
  • 如上述代码所示,我们向一个容量为2的队列中通过offer 添加元素,当添加第三个时,则会反馈 false ,如下结果:

true
true
false

poll

  • poll 方法对应上面 remove 方法,两者的区别就在于是否会在无元素情况下抛出异常,poll 方法在无元素时不会抛出异常而是返回null ,如下代码:
    private static void groupPoll() {
BlockingQueue queue = new ArrayBlockingQueue(2);
System.out.println(queue.offer("云栖简码")); //添加元素
System.out.println(queue.poll()); //取出头元素并且删除
System.out.println(queue.poll()); }
  • 上面代码中我们创建一个容量为2的队列,并添加一个元素,之后调用两次poll方法来获取并删除头节点,发现第二次调用时为null ,因为队列中已经为空了,如下:

true
云栖简码
null

peek

  • peek 方法与前面的 element 方法是对应的 ,获取元素头节点但不删除,与其不同的在于peek 方法在空队列下并不会抛出异常,而是返回 null,如下:
    private static void groupPeek() {
BlockingQueue queue = new ArrayBlockingQueue(2);
System.out.println(queue.offer(1));
System.out.println(queue.peek());
System.out.println(queue.peek());
}
private static void groupPeek2() {
BlockingQueue queue = new ArrayBlockingQueue(2);
System.out.println(queue.peek());
}
  • 如上述代码所示,我么们分别展示了非空队列与空队列下peek 的使用,结果如下:

阻塞类型[put、take]

put

  • put 方法是向队列中添加一个元素,这个方法是阻塞的,也就是说当队列已经满的情况下,再put元素时则会阻塞,直到队列中有空位.

take

  • take 方法是从队列中获取头节点并且将其移除,这也是一个阻塞方法,当队列中已经没有元素时,take 方法则会进入阻塞状态,直到队列中有新的元素进入。

常见的阻塞队列

ArrayBlockingQueue

  • ArrayBlockingQueue 是一个我们常用的典型的有界队列,其内部的实现是基于数组来实现的,我们在创建时需要指定其长度,它的线程安全性由 ReentrantLock 来实现的。
public ArrayBlockingQueue(int capacity) {...}
public ArrayBlockingQueue(int capacity, boolean fair) {...}
  • 如上所示,ArrayBlockingQueue 提供的构造函数中,我们需要指定队列的长度,同时我们也可以设置队列是都是公平的,当我们设置了容量后就不能再修改了,符合数组的特性,此队列按照先进先出(FIFO)的原则对元素进行排序。
  • ReentrantLock 一样,如果 ArrayBlockingQueue 被设置为非公平的,那么就存在插队的可能;如果设置为公平的,那么等待了最长时间的线程会被优先处理,其他线程不允许插队,不过这样的公平策略同时会带来一定的性能损耗,因为非公平的吞吐量通常会高于公平的情况。

LinkedBlockingQueue

  • 从它的名字我们可以知道,它是一个由链表实现的队列,这个队列的长度是 Integer.MAX_VALUE ,这个值是非常大的,几乎无法达到,对此我们可以认为这个队列基本属于一个无界队列(也又认为是有界队列)。此队列按照先进先出的顺序进行排序。

SynchronousQueue

  • synchronousQueue 是一个不存储任何元素的阻塞队列,每一个put操作必须等待take操作,否则不能添加元素。同时它也支持公平锁和非公平锁。
  • synchronousQueue 的容量并不是1,而是0。因为它本身不会持有任何元素,它是直接传递的,synchronousQueue 会把元素从生产者直接传递给消费者,在这个过程中能够是不需要存储的
  • 在我们之前介绍过的线程池 CachedThreadPool 就是利用了该队列。Executors.newCachedThreadPool(),因为这个线程池它的最大线程数是Integer.MAX_VALUE,它是更具需求来创建线程,所有的线程都是临时线程,使用完后空闲60秒则被回收,

PriorityBlockingQueue

  • PriorityBlockingQueue 是一个支持优先级排序的无界阻塞队列,可以通过自定义实现 compareTo() 方法来指定元素的排序规则,或者通过构造器参数 Comparator 来指定排序规则。但是需要注意插入队列的对象必须是可比较大小的,也就是 Comparable 的,否则会抛出 ClassCastException 异常。
  • 它的 take 方法在队列为空的时候会阻塞,但是正因为它是无界队列,而且会自动扩容,所以它的队列永远不会满,所以它的 put 方法永远不会阻塞,添加操作始终都会成功

DelayQueue

  • DelayQueue 是一个实现PriorityBlockingQueue的延迟获取的无界队列。具有“延迟”的功能。
  • DelayQueue 应用场景:1. 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。2. 定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。
  • 它是无界队列,放入的元素必须实现 Delayed 接口,而 Delayed 接口又继承了 Comparable 接口,所以自然就拥有了比较和排序的能力,代码如下:
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
  • 可以看出 Delayed 接口继承 Comparable,里面有一个需要实现的方法,就是  getDelay。这里的 getDelay 方法返回的是“还剩下多长的延迟时间才会被执行”,如果返回 0 或者负数则代表任务已过期。
  • 元素会根据延迟时间的长短被放到队列的不同位置,越靠近队列头代表越早过期。

本文由AnonyStar 发布,可转载但需声明原文出处。

欢迎关注微信公账号 :云栖简码 获取更多优质文章

更多文章关注笔者博客 :云栖简码 i-code.online

JAVA中常见的阻塞队列详解的更多相关文章

  1. Java 中的异常和处理详解

    Java 中的异常和处理详解 原文出处: 代码钢琴家 简介 程序运行时,发生的不被期望的事件,它阻止了程序按照程序员的预期正常执行,这就是异常.异常发生时,是任程序自生自灭,立刻退出终止,还是输出错误 ...

  2. Java中的多线程技术全面详解

    本文主要从整体上介绍Java中的多线程技术,对于一些重要的基础概念会进行相对详细的介绍,若有叙述不清晰或是不正确的地方,希望大家指出,谢谢大家:) 为什么使用多线程 并发与并行 我们知道,在单核机器上 ...

  3. 关于Java中进程和线程的详解

    一.进程:是程序的一次动态执行,它对应着从代码加载,执行至执行完毕的一个完整的过程,是一个动态的实体,它有自己的生命 周期.它因创建而产生,因调度而运行,因等待资源或事件而被处于等待状态,因完成任务而 ...

  4. java中ReentrantLock核心源码详解

    ReentrantLock简介 ReentrantLock是一个可重入且独占式的锁,它具有与使用synchronized监视器锁相同的基本行为和语义,但与synchronized关键字相比,它更灵活. ...

  5. JAVA中IO和NIO的详解分析,内容来自网络和自己总结

    用一个例子来阐释: 一辆客车上有10个乘客,他们的目的地各不相同,当没有售票员的时候,司机就需要不断的询问每一站是否有乘客需要下车,需要则停下,不需要则继续开车,这种就是阻塞的方式. 当有售票员的时候 ...

  6. Java中23种经典设计模式详解

    Java中23种设计模式目录1. 设计模式 31.1 创建型模式 41.1.1 工厂方法 41.1.2 抽象工厂 61.1.3 建造者模式 101.1.4 单态模式 131.1.5 原型模式 151. ...

  7. 2018.8.1 Java中的反射和同步详解

    为何要使用同步? java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查), 将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他 ...

  8. Java中的IO流系统详解(转载)

    摘要: Java 流在处理上分为字符流和字节流.字符流处理的单元为 2 个字节的 Unicode 字符,分别操作字符.字符数组或字符串,而字节流处理单元为 1 个字节,操作字节和字节数组. Java ...

  9. Java中String 的equals 和==详解

    一.Java中数据存储区域包括: 1.寄存器:最快的存储区,由编译器根据需求进行分配,我们在程序中无法控制. 2. 栈:存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new ...

随机推荐

  1. ABP vnext模块化架构的最佳实践的实现

    在上一篇文章<手把手教你用Abp vnext构建API接口服务>中,我们用ABP vnext实现了WebAPI接口服务,但是并非ABP模块化架构的最佳实践.我本身也在学习ABP,我认为AB ...

  2. RLP序列化算法

    RLP RLP(Recursive Length Prefix)递归长度前缀编码,是由以太坊提出的序列化/反序列化标准,相比json格式体积更小,相比protobuf对多语言的支持更强. RLP将数据 ...

  3. scp带密码拷贝文件

    应用场景:将B服务器的文件传输到A服务器.核心命令: sshpass -p 123456 scp ubuntu@192.168.52.1:/home/ubuntu/"TEST"'' ...

  4. python爬虫获取下一页

    from time import sleep import faker import requests from lxml import etree fake = faker.Faker() base ...

  5. Vue实例中封装api接口的思路 在页面中用async,await调用方法请求

    一般我们写小型的项目是用不到封装axios实例 但是当我们写大型项目时  接口有时候多到有上百个接口,那我们在请求一次调用一次接口,接口上好多都是重复的,这个时候我们就可以封装axios实例,既节省了 ...

  6. SSM中 spring-mvc.xml 配置文件

    <!--扫描控制器包--><context:component-scan base-package="<!--控制器包所在路径-->">< ...

  7. Vue基础(1)

    Vue简介 1.JavaScript框架 2.简化Dom操作 3.响应式数据驱动 Vue基础 通过下面代码引用vue: <script src="https://cdn.jsdeliv ...

  8. Libevent库基础(1)

    1.创建 eevent_base struct event_base *base = event_base_new(); 2.创建 事件event struct event *ev; struct e ...

  9. npm的使用说明

    博主是刚开始写项目的前端小白菜,边学边整理,以供后面的小猿参考,共同进步. 首先: npm的官网地址:https://www.npmjs.com Windows 安装包(.msi) 32 位安装包下载 ...

  10. AWK实现把一个文件根据内容进行分组输出多个文件

    AWK实现把一个文件根据内容进行分组输出多个文件 1.首先准备文件data.txt(分隔符为tab) 第一列省编码,第二列省名称...... 2.将该大文件根据第一列的省编码进行分组并输出到各个省编码 ...