并发编程之:BlockingQueue
大家好,我是小黑,一个在互联网苟且偷生的农民工。
队列
学过数据结构的同学应该都知道,队列是数据结构中一种特殊的线性表结构,和平时使用的List,Set这些数据结构相比有点特殊,它的特殊之处在于它只允许在队列的头部(Head)进行删除操作,在尾部(Tail)进行插入操作,这种方式的队列我们称之为先进先出队列(FIFO)。
在JDK1.5
中推出了队列这一数据结构的具体实现,接口Queue是对于队列的定义,并有一些列具有特殊功能的队列实现。
在Queue接口中定义了队列的如下方法:
其中add(E)
并非Queue接口新定义,而是从Collection接口继承而来的。
阻塞队列
BlockingQueue接口也是在JDK1.5
中推出,存放在java.util.concurrent
包中,继承自Queue,所以在BlockingQueue中有Queue的所有方法。
从名字就可以看出BlockingQueue是一种阻塞队列,它支持在检索元素时如果队列为空可以一直阻塞等待直到有元素可以获取,同样在添加元素时如果队列已满会阻塞等待队列中有空闲的存储空间。
BlockingQueue的方法可以归纳为四类:
- 在操作时如不能立即满足,会直接抛出异常
- 在操作时如不能立即满足,则返回特殊的值,如插入、移除方法会返回false,检查方法会返回null
- 在操作时如不能立即满足,则会阻塞等待,直到操作成功
- 在操作时如不能立即满足,则会阻塞等待给定的时间长度,时间到达后如果还不能满足则返回null
这四类方法总结如下。
因为在BlockingQueue的一些方法中,会通过null表示某种操作的失败,所以不允许在BlockingQueue中存放null值元素,会在操作时抛出NullPointerExection异常。
BlockingQueue因为是一个容器嘛,所以它也有容量的限制,在具体实现类中有可以设置容量的实现类,也有不可以设置容量的实现类,不能设置容量的实现类容量默认为Integer.MAX_VALUE
。
BlockingQueue是定义在java.util.concurrent
包中,那么它在并发情况下到底是不是线程安全的呢?
在JDK提供的BlockingQueue的具体实现类中,上面表格中的方法实现都是线程安全的,在内部都使用了锁或者其他形式的并发控制保证操作的原子性。
但是有一点要注意,就是一些批量处理的方法例如addAll
、containsAll
、retainAll
和removeAll
这些方法并不一定是线程安全的,使用时注意。
说完BlockingQueue接口我们接下来看看它都有哪些具体的实现呢?以及在它们内部是如何做到线程安全和阻塞的呢?
ArrayBlockingQueue
ArrayBlockingQueue是一个底层由数组支持额有界阻塞队列。
重要属性
先来看看ArrayBlockingQueue中都有哪些属性。
// 存放元素的数组
final Object[] items;
// 用来记录取元素的下标,用于下一次在take,poll,remove,peek方法中使用
int takeIndex;
// 用来记录添加元素的下标,用于下一次put,offer,add等方法使用
int putIndex;
// 记录队列中元素数量
int count;
// 用于控制并发访问时保证线程安全的锁
final ReentrantLock lock;
// 用于队列空时阻塞和唤醒等待线程的条件
private final Condition notEmpty;
// 用于队列满时阻塞和唤醒等待线程的条件
private final Condition notFull;
我们通过这些队列中的属性基本可以知道ArrayBlockingQueue中都有哪些重要信息,可以看出ArrayBlockingQueue就是使用Object[]
来存放元素的。
那么应该如何创建一个ArrayBlockingQueue呢?
构造方法
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
默认的构造方法需要传入一个int类型的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();
}
从这个方法我们看出传入的false表示会在内部用于创建一个ReentrantLock对象,我们都知道ReentrantLock支持公平和非公平的实现,我们猜想一下,这里的这个fair值是不是表示该阻塞队列对于阻塞排队的线程支持公平和非公平的策略呢?这里先卖个关子,在后面的方法中我们具体说。
除了这两种创建的方式,ArrayBlockingQueue还支持传入一个Collection集合。
public ArrayBlockingQueue(int capacity, boolean fair,
Collection<? extends E> c) {
// 先创建一个ArrayBlockingQueue实例
this(capacity, fair);
final ReentrantLock lock = this.lock;
lock.lock();
try {
int i = 0;
try {
// 循环将collection中的元素放入queue中
for (E e : c) {
checkNotNull(e);
items[i++] = e;
}
} catch (ArrayIndexOutOfBoundsException ex) {
// 如果collection的元素个数超出queue的容量大小,会抛出异常
throw new IllegalArgumentException();
}
count = i;
putIndex = (i == capacity) ? 0 : i;
} finally {
lock.unlock();
}
}
添加元素
先来看看添加一个新元素到ArrayBlockingQueue
是如何实现的,怎样保证线程安全的。
add(e)
public boolean add(E e) {
// 调用父类中的add(e)方法
return super.add(e);
}
public boolean add(E e) {
// 这里会直接调用offer(e)方法,如果offer方法返回false,则直接抛出异常
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
add方法的实现逻辑本质上是对offer方法套了一层壳,如果offer方法返回false时,抛出异常。所以我们直接看offer方法的实现就好。
offer(e)
public boolean offer(E e) {
// 这里先判断空,如果e为空会抛出空指针异常
checkNotNull(e);
final ReentrantLock lock = this.lock;
// 加锁,保证入队操作的原子性
lock.lock();
try {
// 队列满时直接返回false
if (count == items.length)
return false;
else {
// 元素入队
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
可以看到offer方法的逻辑还是比较简单的,先检查入参不能为空,然后加锁保证入队操作的原子性,在获取锁成功后入队,如果队列已满则直接返回false,所以offer方法并不会阻塞。
put(e)
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
// 可被中断方式获取锁
lock.lockInterruptibly();
try {
while (count == items.length)
// 队列满时会阻塞
notFull.await();
// 入队
enqueue(e);
} finally {
lock.unlock();
}
}
put方法和offer方法唯一的区别,就是会在队列满的时候使用Condition条件对象notFull
阻塞等待。
private void enqueue(E x) {
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
// 入队成功,唤醒等待的移除元素操作线程
notEmpty.signal();
}
在enqueue方法中才会完成对队列中的数组元素的赋值动作,完成之后唤醒阻塞等待的移除元素操作线程。
offer(e,time,unit)
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
checkNotNull(e);
// 加锁之前先获取需要等待的时间值
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length) {
// 时间小于等于0时,返回false
if (nanos <= 0)
return false;
// 阻塞等待指定时间
nanos = notFull.awaitNanos(nanos);
}
enqueue(e);
return true;
} finally {
lock.unlock();
}
}
offer(e,time,unit)
方法与offer(e)方法相比,主要时多了一个等待时间,会在时间到达时如果没有空间添加元素返回false。
移除元素
ArrayBlockingQueue中移除元素的方法主要有remove(),poll(),take(),poll(time,unit)四个。这几个方法的实现逻辑都比较简单,这里不在单独贴代码 。我们来看一下阻塞方法take()
的实现即可。
take()
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
// 加锁
lock.lockInterruptibly();
try {
while (count == 0)
// 如果元素数量==0,表示队列中为空,则阻塞等待
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
dequeue()
private E dequeue() {
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
// 取出元素之后,唤醒其他等待线程。
notFull.signal();
return x;
}
LinkedBlockingQueue
LinkedBlockingQueue是一个基于链表结构的阻塞队列,可以在创建时指定边界大小,也可以不指定,在不指定边界时容量为
Integer.MAX_VALUE
。
重要属性
我们先来看看在LinkedBlockingQueue
中都有哪些重要的属性。
// 内部类Node节点,用来存放链表中的元素
static class Node<E> {
// 节点元素
E item;
// 当前节点的下一个节点,如果为空表示没有下一个节点
Node<E> next;
Node(E x) { item = x; }
}
// 队列的容量
private final int capacity;
// 队列中元素的数量
private final AtomicInteger count = new AtomicInteger();
// 头节点
transient Node<E> head;
// 最后一个节点
private transient Node<E> last;
// 获取元素时控制线程安全的锁
private final ReentrantLock takeLock = new ReentrantLock();
// 添加元素时控制线程安全的锁
private final ReentrantLock putLock = new ReentrantLock();
// 控制消费者的条件
private final Condition notEmpty = takeLock.newCondition();
// 控制生产者的条件
private final Condition notFull = putLock.newCondition();
在LinkedBlockingQueue
中使用Node
来存放元素,和指向下一个节点的链表指针。
构造方法
在LinkedBlockingQueue
的构造方法中,会创建一个创建一个不存放元素的Node
对象赋值给head
和last
。
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
// 创建一个不存放元素的Node对象赋值给head和last
last = head = new Node<E>(null);
}
public LinkedBlockingQueue(Collection<? extends E> c) {
this(Integer.MAX_VALUE);
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
int n = 0;
for (E e : c) {
if (e == null)
throw new NullPointerException();
if (n == capacity)
throw new IllegalStateException("Queue full");
// 入队
enqueue(new Node<E>(e));
++n;
}
count.set(n);
} finally {
putLock.unlock();
}
}
添加元素
offer(e)
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
if (count.get() == capacity)
return false;
int c = -1;
Node<E> node = new Node<E>(e);
// 使用putLock加锁
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
if (count.get() < capacity) {
// 入队
enqueue(node);
// 数量+1
c = count.getAndIncrement();
if (c + 1 < capacity)
// 唤醒一个生产者线程
notFull.signal();
}
} finally {
putLock.unlock();
}
if (c == 0)
// 唤醒消费者线程
signalNotEmpty();
// 入队失败情况会返回false
return c >= 0;
}
对于链表结构的LinkedBlockingQueue
来说,入队操作要简单很多,只需要将node节点挂在最后一个节点last的next,然后将自己赋值给last。
private void enqueue(Node<E> node) {
last = last.next = node;
}
put(e)
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
Node<E> node = new Node<E>(e);
// 使用putLock加锁
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
// 如果队列容量已使用完则阻塞
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
对比结果也和我们最开始的方法汇总表格一样,offer(e)
方法会在入队时如果队列已满直接返回false,而put(e)
会一直阻塞等待,知道入队成功。
add(e)
方法和offer(e,time,unit)
方法实现逻辑上没有特殊之处,这里不再放源码。
移除元素
poll()
public E poll() {
final AtomicInteger count = this.count;
if (count.get() == 0)
return null;
E x = null;
int c = -1;
// 使用takeLock加锁
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
if (count.get() > 0) {
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
// 还有元素时唤醒一个生产者线程
notEmpty.signal();
}
} finally {
takeLock.unlock();
}
if (c == capacity)
// 唤醒生产者线程
signalNotFull();
return x;
}
poll()方法会在元素出队时如果没有元素则直接返回null。
// 出队方法
private E dequeue() {
Node<E> h = head;
Node<E> first = h.next;
h.next = h;
head = first;
E x = first.item;
first.item = null;
return x;
}
take()
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
// 使用takeLock加锁
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
//阻塞等待
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
// 还有元素时唤醒一个消费者线程
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
// 唤醒生产者线程
signalNotFull();
return x;
}
同样,take方法会在没有元素时一直等待。
对比
我们来对比一下ArrayBlockingQueue和LinkedBlockingQueue都有哪些区别。
- ArrayBlockingQueue基于数组实现,LinkedBlockingQueue基于链表实现
- ArrayBlockingQueue在添加和移除元素的操作中共用一把锁,LinkedBlockingQueue使用
takeLock
和putLock
两把锁 - ArrayBlockingQueue在添加和移除元素时直接使用元素的类型处理,LinkedBlockingQueue需要转成Node对象
- ArrayBlockingQueue创建时必须指定容量,LinkedBlockingQueue可以不指定,默认容量为
Integer.MAX_VALUE
由于LinkedBlockingQueue使用两把锁将入队操作和出队操作分离,这会大大提高队列的吞吐量,在高并发情况下生产者和消费者可以并行处理,提高并发性能。
但是LinkedBlockingQueue默认是无界队列,要小心内存溢出风险,所以最好在创建时指定容量大小。
BlockingQueue接口的实现类除了本期介绍的这两种,还有PriorityBlockingQueue
,SynchronousQueue
,LinkedBlockingDeque
等,每一个都有它独特的特性和使用场景,后面我们再单独深入解析。
好的,本期内容就到这里,我们下期见,关注我的公众号【小黑说Java】,更多干货内容。
并发编程之:BlockingQueue的更多相关文章
- [转载]并发编程之Operation Queue和GCD
并发编程之Operation Queue http://www.cocoachina.com/applenews/devnews/2013/1210/7506.html 随着移动设备的更新换代,移动设 ...
- Java并发编程之CAS
CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术.简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替 ...
- 并发编程之wait()、notify()
前面的并发编程之volatile中我们用程序模拟了一个场景:在main方法中开启两个线程,其中一个线程t1往list里循环添加元素,另一个线程t2监听list中的size,当size等于5时,t2线程 ...
- 并发编程之 Exchanger 源码分析
前言 JUC 包中除了 CountDownLatch, CyclicBarrier, Semaphore, 还有一个重要的工具,只不过相对而言使用的不多,什么呢? Exchange -- 交换器.用于 ...
- 并发编程之 Condition 源码分析
前言 Condition 是 Lock 的伴侣,至于如何使用,我们之前也写了一些文章来说,例如 使用 ReentrantLock 和 Condition 实现一个阻塞队列,并发编程之 Java 三把锁 ...
- python并发编程之Queue线程、进程、协程通信(五)
单线程.多线程之间.进程之间.协程之间很多时候需要协同完成工作,这个时候它们需要进行通讯.或者说为了解耦,普遍采用Queue,生产消费模式. 系列文章 python并发编程之threading线程(一 ...
- python并发编程之gevent协程(四)
协程的含义就不再提,在py2和py3的早期版本中,python协程的主流实现方法是使用gevent模块.由于协程对于操作系统是无感知的,所以其切换需要程序员自己去完成. 系列文章 python并发编程 ...
- python并发编程之asyncio协程(三)
协程实现了在单线程下的并发,每个协程共享线程的几乎所有的资源,除了协程自己私有的上下文栈:协程的切换属于程序级别的切换,对于操作系统来说是无感知的,因此切换速度更快.开销更小.效率更高,在有多IO操作 ...
- python并发编程之multiprocessing进程(二)
python的multiprocessing模块是用来创建多进程的,下面对multiprocessing总结一下使用记录. 系列文章 python并发编程之threading线程(一) python并 ...
- python并发编程之threading线程(一)
进程是系统进行资源分配最小单元,线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.进程在执行过程中拥有独立的内存单元,而多个线程共享内存等资源. 系列文章 py ...
随机推荐
- Tomcat PUT方法任意写文件漏洞(CVE-2017-12615)
Apache Tomcat 7.0.0~7.0.79 直接发送以下数据包即可在Web根目录写入shell: PUT /1.jsp/ HTTP/1.1 Host: 192.168.49.2:8080 A ...
- .NET5控制台程序使用EF连接MYSQL
.NET5控制台程序使用EF连接MYSQL 1.使用nuget下载: Microsoft.Extensions.Configuration.Json Pomelo.EntityFrameworkCor ...
- ContentObserver 内容观察者作用及特点
eg: 1.定义Uri public static Uri KEY_BROWSER_URI = Uri.parse("content://com.android.browser.provid ...
- Android中的一些小细节
在代码中获取到的padding .margin.getWidth.event.getX.context.getResources().getDimensionPixelSize(R.dimen.key ...
- 查看filesystem type及ftype的值
查看Filesystem type df -Th 查看ftype xfs_info [filesystem_name] | grep ftype
- Python 和 C/C++ 拓展程序如何性能优化?看这一篇文就够
作者:王璐璐 | 旷视 MegEngine 架构师 一. 背景 在 MegEngine imperative runtime 的早期开发中,我们面临着一些的性能优化问题.除了一些已知需要重构的地方(早 ...
- Android系统编程入门系列之服务Service齐头并进多线程任务
在上篇文章中初步了解了Android系统的四大组件之一的服务Service,在服务内可以执行无用户交互的耗时操作任务,但是包括之前关于界面系列文章在内,生命周期方法都是在主线程内被系统回调的.如果直接 ...
- 就这?分布式 ID 发号器实战
分布式 ID 需要满足的条件: 全局唯一:这是最基本的要求,必须保证 ID 是全局唯一的. 高性能:低延时,不能因为一个小小的 ID 生成,影响整个业务响应速度. 高可用:无限接近于100%的可用性. ...
- 免费个人图床搭建gitee+PicGo
我们写博客的时候经常会需要配图,特别是markdown写的时候只能通过网络链接来展示图片. 首先来说存储仓库.测试过几款存储图片的仓库,最终选择方案3: 1.阿里OSS需要付费,空间和流量双向收费,对 ...
- noip19
sb\(O(n^{2})\)传参 T1 暴力一会儿就码好,结果.. 祭奠一下死去的代码 died #include<cstdio> #define MAX 1010 #define re ...