开篇先介绍下 BlockingQueue 这个接口的规则,后面再看其实现。

阻塞队列概要

阻塞队列与我们平常接触的普通队列(LinkedList或ArrayList等)的最大不同点,在于阻塞队列的阻塞添加和阻塞删除方法。

阻塞添加
所谓的阻塞添加是指当阻塞队列元素已满时,队列会阻塞加入元素的线程,直队列元素不满时才重新唤醒线程执行元素加入操作。

阻塞删除
阻塞删除是指在队列元素为空时,删除队列元素的线程将被阻塞,直到队列不为空再执行删除操作(一般都会返回被删除的元素)。

由于Java中的阻塞队列接口BlockingQueue继承自Queue接口,因此先来看看阻塞队列接口为我们提供的主要方法

 public interface BlockingQueue<E> extends Queue<E> {

     //将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量)
//在成功时返回 true,如果此队列已满,则抛IllegalStateException。
boolean add(E e); //将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量)
// 将指定的元素插入此队列的尾部,如果该队列已满,
//则在到达指定的等待时间之前等待可用的空间,该方法可中断
boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException; //将指定的元素插入此队列的尾部,如果该队列已满,则一直等到(阻塞)。
void put(E e) throws InterruptedException; //获取并移除此队列的头部,如果没有元素则等待(阻塞),
//直到有元素将唤醒等待线程执行该操作
E take() throws InterruptedException; //获取并移除此队列的头部,在指定的等待时间前一直等到获取元素, //超过时间方法将结束
E poll(long timeout, TimeUnit unit) throws InterruptedException; //从此队列中移除指定元素的单个实例(如果存在)。
boolean remove(Object o);
}

这里我们把上述操作进行分类

插入方法:

  add(E e) : 添加成功返回true,失败抛IllegalStateException异常
  offer(E e) : 成功返回 true,如果此队列已满,则返回 false。
  put(E e) :将元素插入此队列的尾部,如果该队列已满,则一直阻塞
删除方法:

  remove(Object o) :移除指定元素,成功返回true,失败返回false
  poll() : 获取并移除此队列的头元素,若队列为空,则返回 null
  take():获取并移除此队列头元素,若没有元素则一直阻塞。

阻塞队列的对元素的增删查操作主要就是上述的三类方法,通常情况下我们都是通过这3类方法操作阻塞队列,了解完阻塞队列的基本方法后,下面我们将分析阻塞队列中的两个实现类ArrayBlockingQueue和LinkedBlockingQueue的简单使用和实现原理,其中实现原理是这篇文章重点分析的内容。

ArrayBlockingQueue

在看源码之前,通过查询API发现对ArrayBlockingQueue特点的简单介绍:

1、一个由数组支持的有界队列,此队列按FIFO(先进先出)原则对元素进行排序。
2、新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素
3、这是一个简单的“有界缓存区”,一旦创建,就不能在增加其容量
4、在向已满队列中添加元素会导致操作阻塞,从空队列中提取元素也将导致阻塞
5、此类支持对等待的生产者线程和使用者线程进行排序的可选公平策略。默认情况下,不保证是这种排序的。然而通过将公平性(fairness)设置为true,而构造的队列允许按照FIFO顺序访问线程。公平性通常会降低吞吐量,但也减少了可变性和避免了“不平衡性”。

简单的来说,ArrayBlockingQueue 是一个用数组实现的有界阻塞队列,其内部按先进先出的原则对元素进行排序,其中put方法和take方法为添加和删除的阻塞方法,下面我们通过ArrayBlockingQueue队列实现一个生产者消费者的案例,通过该案例简单了解其使用方式

使用示例

Consumer 消费者和 Producer 生产者,通过ArrayBlockingQueue 队列获取和添加元素,其中消费者调用了take()方法获取元素当队列没有元素就阻塞,生产者调用put()方法添加元素,当队列满时就阻塞,通过这种方式便实现生产者消费者模式。比直接使用等待唤醒机制或者Condition条件队列来得更加简单。

 package com.zejian.concurrencys.Queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit; /**
* Created by chenhao on 2018/01/07
*/
public class ArrayBlockingQueueDemo {
private final static ArrayBlockingQueue<Apple> queue= new ArrayBlockingQueue<>(1);
public static void main(String[] args){
new Thread(new Producer(queue)).start();
new Thread(new Producer(queue)).start();
new Thread(new Consumer(queue)).start();
new Thread(new Consumer(queue)).start();
}
} class Apple {
public Apple(){
}
} /**
* 生产者线程
*/
class Producer implements Runnable{
private final ArrayBlockingQueue<Apple> mAbq;
Producer(ArrayBlockingQueue<Apple> arrayBlockingQueue){
this.mAbq = arrayBlockingQueue;
} @Override
public void run() {
while (true) {
Produce();
}
} private void Produce(){
try {
Apple apple = new Apple();
mAbq.put(apple);
System.out.println("生产:"+apple);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} /**
* 消费者线程
*/
class Consumer implements Runnable{ private ArrayBlockingQueue<Apple> mAbq;
Consumer(ArrayBlockingQueue<Apple> arrayBlockingQueue){
this.mAbq = arrayBlockingQueue;
} @Override
public void run() {
while (true){
try {
TimeUnit.MILLISECONDS.sleep(1000);
comsume();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} private void comsume() throws InterruptedException {
Apple apple = mAbq.take();
System.out.println("消费Apple="+apple);
}
}

输出:

 生产:com.zejian.concurrencys.Queue.Apple@109967f
消费Apple=com.zejian.concurrencys.Queue.Apple@109967f
生产:com.zejian.concurrencys.Queue.Apple@269a77
生产:com.zejian.concurrencys.Queue.Apple@1ce746e
消费Apple=com.zejian.concurrencys.Queue.Apple@269a77
消费Apple=com.zejian.concurrencys.Queue.Apple@1ce746e
........

源码剖析

ArrayBlockingQueue内部的阻塞队列是通过重入锁ReenterLock和Condition条件队列实现的,所以ArrayBlockingQueue中的元素存在公平访问与非公平访问的区别,对于公平访问队列,被阻塞的线程可以按照阻塞的先后顺序访问队列,即先阻塞的线程先访问队列。而非公平队列,当队列可用时,阻塞的线程将进入争夺访问资源的竞争中,也就是说谁先抢到谁就执行,没有固定的先后顺序。创建公平与非公平阻塞队列代码如下:

 //默认非公平阻塞队列
ArrayBlockingQueue queue = new ArrayBlockingQueue(2);
//公平阻塞队列
ArrayBlockingQueue queue1 = new ArrayBlockingQueue(2,true); //构造方法源码
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
} 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();
}

ArrayBlockingQueue的内部是通过一个可重入锁ReentrantLock和两个Condition条件对象来实现阻塞,这里先看看其内部成员变量

 public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable { /** 存储数据的数组 */
final Object[] items; /**获取数据的索引,主要用于take,poll,peek,remove方法 */
int takeIndex; /**添加数据的索引,主要用于 put, offer, or add 方法*/
int putIndex; /** 队列元素的个数 */
int count; /** 控制并非访问的锁 */
final ReentrantLock lock; /**notEmpty条件对象,用于通知take方法队列已有元素,可执行获取操作 */
private final Condition notEmpty; /**notFull条件对象,用于通知put方法队列未满,可执行添加操作 */
private final Condition notFull; /**
迭代器
*/
transient Itrs itrs = null; }

从成员变量可看出,ArrayBlockingQueue内部确实是通过数组对象items来存储所有的数据,值得注意的是ArrayBlockingQueue通过一个ReentrantLock来同时控制添加线程与移除线程的并非访问,这点与LinkedBlockingQueue区别很大(稍后会分析)。而对于notEmpty条件对象则是用于存放等待或唤醒调用take方法的线程,告诉他们队列已有元素,可以执行获取操作。同理notFull条件对象是用于等待或唤醒调用put方法的线程,告诉它们,队列未满,可以执行添加元素的操作。takeIndex代表的是下一个方法(take,poll,peek,remove)被调用时获取数组元素的索引,putIndex则代表下一个方法(put, offer, or add)被调用时元素添加到数组中的索引。图示如下

添加

 //add方法实现,间接调用了offer(e)
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
} //offer方法
public boolean offer(E e) {
checkNotNull(e);//检查元素是否为null
final ReentrantLock lock = this.lock;
lock.lock();//加锁
try {
if (count == items.length)//判断队列是否满
return false;
else {
enqueue(e);//添加元素到队列
return true;
}
} finally {
lock.unlock();
}
} //入队操作
private void enqueue(E x) {
//获取当前数组
final Object[] items = this.items;
//通过putIndex索引对数组进行赋值
items[putIndex] = x;
//索引自增,如果已是最后一个位置,重新设置 putIndex = 0;
if (++putIndex == items.length)
putIndex = 0;
count++;//队列中元素数量加1
//唤醒调用take()方法的线程,执行元素获取操作。
notEmpty.signal();
}

这里的add方法和offer方法实现比较简单,其中需要注意的是enqueue(E x)方法,当putIndex索引大小等于数组长度时,需要将putIndex重新设置为0,因为后面讲到的取值也是从数组中第一个开始依次往后面取,取了之后会将原位置的值设置为null,方便循环put操作,这里要注意并不是每次都是取数组中的第一个值,takeIndex也会增加。因为做了添加操作,数组中肯定不会空,则 notEmpty条件会唤醒take()方法取值。

ok~,接着看put方法,它是一个阻塞添加的方法:

 //put方法,阻塞时可中断
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();//该方法可中断
try {
//当队列元素个数与数组长度相等时,无法添加元素
while (count == items.length)
//将当前调用线程挂起,添加到notFull条件队列中等待唤醒
notFull.await();
enqueue(e);//如果队列没有满直接添加。。
} finally {
lock.unlock();
}
}

put方法是一个阻塞的方法,如果队列元素已满,那么当前线程将会被notFull条件对象挂起加到等待队列中,直到队列有空档才会唤醒执行添加操作。但如果队列没有满,那么就直接调用enqueue(e)方法将元素加入到数组队列中。到此我们对三个添加方法即put,offer,add都分析完毕,其中offer,add在正常情况下都是无阻塞的添加,而put方法是阻塞添加。

(获取)删除

关于删除先看poll方法,该方法获取并移除此队列的头元素,若队列为空,则返回 null。

 public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//判断队列是否为null,不为null执行dequeue()方法,否则返回null
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
//删除队列头元素并返回
private E dequeue() {
//拿到当前数组的数据
final Object[] items = this.items;
@SuppressWarnings("unchecked")
//获取要删除的对象
E x = (E) items[takeIndex];
将数组中takeIndex索引位置设置为null
items[takeIndex] = null;
//takeIndex索引加1并判断是否与数组长度相等,
//如果相等说明已到尽头,恢复为0
if (++takeIndex == items.length)
takeIndex = 0;
count--;//队列个数减1
if (itrs != null)
itrs.elementDequeued();//同时更新迭代器中的元素数据
//删除了元素说明队列有空位,唤醒notFull条件对象添加线程,执行添加操作
notFull.signal();
return x;
}

接着看take()方法,是一个阻塞方法,获取队列头元素并删除。

 //从队列头部删除,队列没有元素就阻塞,可中断
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();//中断
try {
//如果队列没有元素
while (count == 0)
//执行阻塞操作
notEmpty.await();
return dequeue();//如果队列有元素执行删除操作
} finally {
lock.unlock();
}
}

take和poll的区别是,队列为空时,poll返回null,take则被挂起阻塞,直到有元素添加进来,take线程被唤醒,然后获取第一个元素并删除。

peek方法非常简单,直接返回当前队列的头元素但不删除任何元素。

 public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//直接返回当前队列的头元素,但不删除
return itemAt(takeIndex); // null when queue is empty
} finally {
lock.unlock();
}
} final E itemAt(int i) {
return (E) items[i];
}

ok~,到此对于ArrayBlockingQueue的主要方法就分析完了。

并发编程(八)—— Java 并发队列 BlockingQueue 实现之 ArrayBlockingQueue 源码分析的更多相关文章

  1. 并发编程(九)—— Java 并发队列 BlockingQueue 实现之 LinkedBlockingQueue 源码分析

    LinkedBlockingQueue 在看源码之前,通过查询API发现对LinkedBlockingQueue特点的简单介绍: 1.LinkedBlockingQueue是一个由链表实现的有界队列阻 ...

  2. 并发编程(十)—— Java 并发队列 BlockingQueue 实现之 SynchronousQueue源码分析

    BlockingQueue 实现之 SynchronousQueue SynchronousQueue是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除 ...

  3. Java核心复习——J.U.C ArrayBlockingQueue源码分析

    介绍 依赖关系 源码 构造方法 public ArrayBlockingQueue(int capacity) { this(capacity, false);//默认构造非公平的有界队列 } pub ...

  4. java多线程系列(九)---ArrayBlockingQueue源码分析

    java多线程系列(九)---ArrayBlockingQueue源码分析 目录 认识cpu.核心与线程 java多线程系列(一)之java多线程技能 java多线程系列(二)之对象变量的并发访问 j ...

  5. 消息队列的一些场景及源码分析,RocketMQ使用相关问题及性能优化

    前文目录链接参考: 消息队列的一些场景及源码分析,RocketMQ使用相关问题及性能优化 https://www.cnblogs.com/yizhiamumu/p/16694126.html 消息队列 ...

  6. java中的==、equals()、hashCode()源码分析(转载)

    在java编程或者面试中经常会遇到 == .equals()的比较.自己看了看源码,结合实际的编程总结一下. 1. ==  java中的==是比较两个对象在JVM中的地址.比较好理解.看下面的代码: ...

  7. Java的三种代理模式&完整源码分析

    Java的三种代理模式&完整源码分析 参考资料: 博客园-Java的三种代理模式 简书-JDK动态代理-超详细源码分析 [博客园-WeakCache缓存的实现机制](https://www.c ...

  8. java 日志体系(四)log4j 源码分析

    java 日志体系(四)log4j 源码分析 logback.log4j2.jul 都是在 log4j 的基础上扩展的,其实现的逻辑都差不多,下面以 log4j 为例剖析一下日志框架的基本组件. 一. ...

  9. Java SPI机制实战详解及源码分析

    背景介绍 提起SPI机制,可能很多人不太熟悉,它是由JDK直接提供的,全称为:Service Provider Interface.而在平时的使用过程中也很少遇到,但如果你阅读一些框架的源码时,会发现 ...

随机推荐

  1. python生成pdf报告、python实现html转换为pdf报告

    1.先说下html转换为pdf:其实支持直接生成,有三个函数pdfkit.f 安装python包:pip Install pdfkit 系统安装wkhtmltopdf:参考 https://githu ...

  2. ViewState 和字段属性的差异

    ViewState是一个字典对象,将控件状态或对象值散列为字符串并在页面中保存为一个或多个隐藏域... PostBack之后字段的值就被清空了而ViewState还在,但ViewState为了保持值需 ...

  3. qt之数据库对照片的存取

    需要确保数据库连接上 QOCI为驱动 //oracle 数据库连接 //需要在执行文件目录添加 oci.dll oraociei11.dll QSqlDatabase db = QSqlDatabas ...

  4. 【Vue-Cli3.0】【1】创建一个Vue-Cli3.0的项目

    最近在做爬虫,然后要爬好多数据,代码写完了,就让它在爬了.不想闲着就复习一下Vue吧! 开始开始! ***正式讲解之前 先下载一个node.js吧! 一.首先检查一下 版本 PS D:\徐孟林\D D ...

  5. HeadDoc自动注释语法

    记录于2013/4/23: 关于HeaderDoc注释和标签的简要介绍 每个HeaderDoc注释可分为顶级标签和第二级标签: (1)顶级标签:宣布API的声明类型 (function, struct ...

  6. tomcat部署项目启动采坑之UnknownHostException

    在一台新服务器上,把war包部署在tomcat上,很普通的很简单的一个活,但我踩到一个大坑. 需要组件tomcat8,mysql5.7,mosqquito1.5,centos7,war包,把组件都装好 ...

  7. C++入门笔记(二)变量和基本类型

    变量和基本类型 一.基本内置类型 1.除去布尔类型和扩展的字符型外,其他整型可以分为带符号的和无符号的. 2.与其他整型不同,字符型被分为了三种:char.signed char 和 unsigned ...

  8. pip离线安装依赖包

    pip安装离线本地包 导出本地已有的依赖包 pip freeze > requirements.txt 将依赖包下载到本地 # 下载到当前目录,指定pip源 pip download -r re ...

  9. 基于vue-cli配置移动端自适应

    移动端自适应:手淘的 lib-flexible + rem 配置 flexible 安装 lib-flexible 在命令行中运行如下安装: 1 npm i lib-flexible --save 引 ...

  10. java枚举使用 总结

    补充几点: 1.枚举对象是可以用 == 比较. 2. TestEnum3反编译结果: F:\tree\Test\src\test>javap TestEnum3* Compiled from & ...