这篇博客南国主要讲解关于Java中阻塞队列的知识点,提到阻塞队列(BlockingQueue)想必大家最先想到的是生产者-消费者,诚然这也是阻塞队列最直接的应用场景。 本篇分为四个章节,BlockingQueue简介,常见的基本操作,常用的BlockingQueue实现类和应用demo。这里针对BlockingQueue的应用南国主要写了生产者-消费者的实现和线程通信的实现。前三个部分的基础知识总结,很多内容参考了并发容器之BlockingQueue的叙述,结合自己的理解南国 在一些内容上做了增加和重新编辑。
话不多说,干货送上~
在实际编程中,会经常使用到JDK中Collection集合框架中的各种容器类如实现List,Map,Queue接口的容器类,但是这些容器类基本上不是线程安全的,除了使用Collections可以将其转换为线程安全的容器,Doug Lea大师为我们都准备了对应的线程安全的容器,如实现List接口的CopyOnWriteArrayList,实现Map接口的ConcurrentHashMap,实现Queue接口的ConcurrentLinkedQueue。
在我们学习操作系统时遇到的一个最经典的"生产者-消费者"问题中,队列通常被视作线程间操作的数据容器,这样,可以对各个模块的业务功能进行解耦,生产者将“生产”出来的数据放置在数据容器中,而消费者仅仅只需要在“数据容器”中进行获取数据即可,这样生产者线程和消费者线程就能够进行解耦,只专注于自己的业务功能即可。阻塞队列(BlockingQueue)被广泛使用在“生产者-消费者”问题中,其原因是BlockingQueue提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。
BlockingQueue基本操作总结如下:
BlockingQueue继承于Queue接口,因此,对数据元素的基本操作有:
插入元素:
add(E e) :往队列插入数据,当队列满时,插入元素时会抛出IllegalStateException异常;
offer(E e):当往队列插入数据时,插入成功返回true,否则则返回false。当队列满时不会抛出异常;
删除元素:
remove(Object o):从队列中删除数据,成功则返回true,否则为false
poll:删除数据,当队列为空时,返回null;
查看元素:
element:获取队头元素,如果队列为空时则抛出NoSuchElementException异常;
peek:获取队头元素,如果队列为空则抛出NoSuchElementException异常
接下来,南国讲一下BlockingQueue具有的特殊操作:
插入数据:
put:当阻塞队列容量已经满时,往阻塞队列插入数据的线程会被阻塞,直至阻塞队列已经有空余的容量可供使用;
offer(E e, long timeout, TimeUnit unit):若阻塞队列已经满时,同样会阻塞插入数据的线程,直至阻塞队列已经有空余的地方,与put方法不同的是,该方法会有一个超时时间,若超过当前给定的超时时间,插入数据的线程会退出;
删除数据:
take():当阻塞队列为空时,获取队头数据的线程会被阻塞;
poll(long timeout, TimeUnit unit):当阻塞队列为空时,获取数据的线程会被阻塞,另外,如果被阻塞的线程超过了给定的时长,该线程会退出
实现BlockingQueue接口的有ArrayBlockingQueue, DelayQueue, LinkedBlockingDeque, LinkedBlockingQueue, LinkedTransferQueue, PriorityBlockingQueue, SynchronousQueue,而这几种常见的阻塞队列也是在实际编程中会常用的,下面对这几种常见的阻塞队列进行说明:
ArrayBlockingQueue是由数组实现的有界阻塞队列。该队列命令元素FIFO(先进先出)。因此,对头元素head是队列中存在时间最长的数据元素,而对尾数据tail则是当前队列最新的数据元素。ArrayBlockingQueue可作为“有界数据缓冲区”,生产者插入数据到队列容器中,并由消费者提取。ArrayBlockingQueue一旦创建,容量不能改变。
当队列容量满时,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞。
ArrayBlockingQueue默认情况下不能保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到ArrayBlockingQueue。而非公平性则是指访问ArrayBlockingQueue的顺序不是遵守严格的时间顺序,有可能存在,一旦ArrayBlockingQueue可以被访问时,长时间阻塞的线程依然无法访问到ArrayBlockingQueue。如果保证公平性,通常会降低吞吐量。如果需要获得公平性的ArrayBlockingQueue,可采用如下代码:
private static ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue(10,true);
ArrayBlockingQueue的主要属性如下:
/** The queued items */
final Object[] items;
/** items index for next take, poll, peek or remove */
int takeIndex;
/** items index for next put, offer, or add */
int putIndex;
/** Number of elements in the queue */
int count;
/*
* Concurrency control uses the classic two-condition algorithm
* found in any textbook.
*/
/** Main lock guarding all access */
final ReentrantLock lock;
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;
从源码中可以看出ArrayBlockingQueue内部是采用数组进行数据存储的(属性items),为了保证线程安全,采用的是ReentrantLock lock,为了保证可阻塞式的插入删除数据利用的是Condition,当获取数据的消费者线程被阻塞时会将该线程放置到notEmpty等待队列中,当插入数据的生产者线程被阻塞时,会将该线程放置到notFull等待队列中。而notEmpty和notFull等中要属性在构造方法中进行创建:
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();
}
接下来,主要看看可阻塞式的put和take方法是怎样实现的。
put(E e)方法源码如下:
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//如果当前队列已满,将线程移入到notFull等待队列中
while (count == items.length)
notFull.await();
//满足插入数据的要求,直接进行入队操作
enqueue(e);
} finally {
lock.unlock();
}
}
该方法的逻辑很简单,当队列已满时(count == items.length)将线程移入到notFull等待队列中,如果当前满足插入数据的条件,就可以直接调用 enqueue(e)插入数据元素。enqueue方法源码为:
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
//插入数据
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
//通知消费者线程,当前队列中有数据可供消费
notEmpty.signal();
}
enqueue方法的逻辑同样也很简单,先完成插入数据,即往数组中添加数据(items[putIndex] = x),然后通知被阻塞的消费者线程,当前队列中有数据可供消费(notEmpty.signal())。
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方法也主要做了两步:1. 如果当前队列为空的话,则将获取数据的消费者线程移入到等待队列中;2. 若队列不为空则获取数据,即完成出队操作dequeue。dequeue方法源码为:
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
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;
}
dequeue方法也主要做了两件事情:1. 获取队列中的数据,即获取数组中的数据元素((E) items[takeIndex]);2. 通知notFull等待队列中的线程,使其由等待队列移入到同步队列中,使其能够有机会获得lock,并执行完成功退出。
从以上分析,可以看出put和take方法主要是通过condition的通知机制来完成可阻塞式的插入数据和获取数据。在理解ArrayBlockingQueue后再去理解LinkedBlockingQueue就很容易了。
LinkedBlockingQueue是用链表实现的阻塞队列,同样满足FIFO的特性。与ArrayBlockingQueue相比起来具有更高的吞吐量,为了防止LinkedBlockingQueue容量迅速增,损耗大量内,通常在创建LinkedBlockingQueue对象时,会指定其大小(如果指定了大小,我们判定它为有界队列),如果未指定,容量等于Integer.MAX_VALUE(我们视它为无界队列)。查看它的构造方法:
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
LinkedBlockingQueue的主要属性有:
/** Current number of elements */
private final AtomicInteger count = new AtomicInteger();
/**
* Head of linked list.
* Invariant: head.item == null
*/
transient Node head;
/**
* Tail of linked list.
* Invariant: last.next == null
*/
private transient Node last;
/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
可以看出与ArrayBlockingQueue主要的区别是,LinkedBlockingQueue在插入数据和删除数据时分别是由两个不同的lock(takeLock和putLock)来控制线程安全的,因此,也由这两个lock生成了两个对应的condition(notEmpty和notFull)来实现可阻塞的插入和删除数据。并且,采用了链表的数据结构来实现队列,Node结点的定义为:
static class Node {
E item;
/**
* One of:
* - the real successor Node
* - this Node, meaning the successor is head.next
* - null, meaning there is no successor (this is the last node)
*/
Node next;
Node(E x) { item = x; }
}
接下来,我们也同样来看看put方法和take方法的实现。
put方法源码为:
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node node = new Node(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
//如果队列已满,则阻塞当前线程,将其移入等待队列
while (count.get() == capacity) {
notFull.await();
}
//入队操作,插入数据
enqueue(node);
c = count.getAndIncrement();
//若队列满足插入数据的条件,则通知被阻塞的生产者线程
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
put方法的逻辑也同样很容易理解,可见注释。基本上和ArrayBlockingQueue的put方法一样。
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
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都是通过condition通知机制来实现可阻塞式插入和删除元素,并满足线程安全的特性;
不同点:1. ArrayBlockingQueue底层是采用的数组进行实现,而LinkedBlockingQueue则是采用链表数据结构; 2. ArrayBlockingQueue插入和删除数据,只采用了一个lock,而LinkedBlockingQueue则是在插入和删除分别采用了putLock和takeLock,这样可以降低线程由于线程无法获取到lock而进入WAITING状态的可能性,从而提高了线程并发执行的效率。
PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现compareTo()方法来指定元素排序规则,或者初始化时通过构造器参数Comparator来指定排序规则。
它的实质是一种无缓冲的等待队列。SynchronousQueue每个插入操作必须等待另一个线程进行相应的删除操作,因此,SynchronousQueue实际上没有存储任何数据元素,因为只有线程在删除数据时,其他线程才能插入数据,同样的,如果当前有线程在插入数据时,线程才能删除数据。SynchronousQueue也可以通过构造器参数来为其指定公平性。
LinkedTransferQueue是一个由链表数据结构构成的无界阻塞队列,由于该队列实现了TransferQueue接口,与其他阻塞队列相比主要有以下不同的方法:
LinkedBlockingDeque是基于链表数据结构的有界阻塞双端队列,如果在创建对象时为指定大小时,其默认大小为Integer.MAX_VALUE。与LinkedBlockingQueue相比,主要的不同点在于,LinkedBlockingDeque具有双端队列的特性。LinkedBlockingDeque基本操作如下图所示:
如上图所示,LinkedBlockingDeque的基本操作可以分为四种类型:1.特殊情况,抛出异常;2.特殊情况,返回特殊值如null或者false;3.当线程不满足操作条件时,线程会被阻塞直至条件满足;4. 操作具有超时特性。
另外,LinkedBlockingDeque实现了BlockingDueue接口而LinkedBlockingQueue实现的是BlockingQueue,这两个接口的主要区别如下图所示:
从上图可以看出,两个接口的功能是可以等价使用的,比如BlockingQueue的add方法和BlockingDeque的addLast方法的功能是一样的。
DelayQueue是一个存放实现Delayed接口的数据的无界阻塞队列,只有当数据对象的延时时间达到时才能插入到队列进行存储。如果当前所有的数据都还没有达到创建时所指定的延时期,则队列没有队头,并且线程通过poll等方法获取数据元素则返回null。所谓数据延时期满时,则是通过Delayed接口的getDelay(TimeUnit.NANOSECONDS)来进行判定,如果该方法返回的是小于等于0则说明该数据元素的延时期已满。
通过前面的学习,相比你已经对阻塞队列以及常用的类型有了一个基本的了解。 下面,南国将两个阻塞应用的最广泛的例子:生产者-消费者模式的实现,实现线程通信
1. 抛开这篇博客提到的阻塞队列,我们手动写一个非阻塞队列的方式实现消费者-生产者模式。
package 并发多线程.生产者_消费者模式;
import java.util.PriorityQueue;
/**
* 使用Object.wait()和Object.notify() 非阻塞队列的方式实现消费者-生产者模式
*
* @author xjh 2019.12.26
*/
public class Wait_Notify {
private int queueSize = 10;
private PriorityQueue queue = new PriorityQueue<>(queueSize);
public static void main(String[] args) {
Wait_Notify wait_notify = new Wait_Notify();
Producer producer =wait_notify.new Producer(); //内部类的对象创建,需要通过外部类对象进行调用
Consumer consumer=wait_notify.new Consumer();
producer.start();
consumer.start();
}
//创建内部类 Producer表示生产者线程相关的类
class Producer extends Thread {
@Override
public void run() {
produce();
}
private void produce() {
while (true) {
synchronized (queue) {
//对代码块进行加锁
while (queue.size() == queueSize) { //队列已满,不能再生产了
System.out.println("the queue is full, please wait...");
try {
queue.wait(); //当前线程挂起,进入等待队列
} catch (InterruptedException e) {
e.printStackTrace();
queue.notify(); //notify 唤醒等待挂起的线程
}
}
queue.offer(1); //入队一个元素
queue.notify();
System.out.println("I have inserted one element, the rest capacity is: " + (queueSize - queue.size()));
}
}
}
}
//创建内部类 Consumer表示消费者线程相关的类
class Consumer extends Thread {
@Override
public void run() {
consume();
}
private void consume() {
while (true) {
synchronized (queue) {
//对代码块进行加锁
while (queue.size() == 0) { //队列为空,不能再消费了
System.out.println("the queue is empty, please wait...");
try {
queue.wait(); //当前线程挂起,进入等待队列
} catch (InterruptedException e) {
e.printStackTrace();
queue.notify(); //notify 唤醒等待挂起的线程
}
}
queue.poll(); //出队一个元素
queue.notify();
System.out.println("I have polled one element, the rest elements are: " + queue.size());
}
}
}
}
}
输出结果:
......
the queue is full, please wait...
I have polled one element, the rest elements are: 9
I have polled one element, the rest elements are: 8
I have polled one element, the rest elements are: 7
I have polled one element, the rest elements are: 6
I have polled one element, the rest elements are: 5
I have polled one element, the rest elements are: 4
I have polled one element, the rest elements are: 3
I have polled one element, the rest elements are: 2
I have polled one element, the rest elements are: 1
I have polled one element, the rest elements are: 0
the queue is empty, please wait...
I have inserted one element, the rest capacity is: 9
I have inserted one element, the rest capacity is: 8
I have inserted one element, the rest capacity is: 7
I have inserted one element, the rest capacity is: 6
I have inserted one element, the rest capacity is: 5
I have inserted one element, the rest capacity is: 4
I have inserted one element, the rest capacity is: 3
I have inserted one element, the rest capacity is: 2
I have inserted one element, the rest capacity is: 1
I have inserted one element, the rest capacity is: 0
.......
2. 使用阻塞队列实现生产者-消费者(这里我使用的是ArrayBlockingQueue)
package 并发多线程.生产者_消费者模式;
import java.util.concurrent.ArrayBlockingQueue;
/**
* 使用ArrayBlockQueue实现生产者消费者模式
* @author xjh 2019.12.26
*/
public class ArrayBlockQueue_Demo {
private int queueSize = 10;
private ArrayBlockingQueue queue = new ArrayBlockingQueue<>(queueSize);
// 非阻塞模式用的PriorityQueue,阻塞模式下我们使用ArrayBlockingQueue
public static void main(String[] args) {
ArrayBlockQueue_Demo arrayBlockQueue_demo = new ArrayBlockQueue_Demo();
Producer producer=arrayBlockQueue_demo.new Producer();
Consumer consumer=arrayBlockQueue_demo.new Consumer();
consumer.start();
producer.start();
}
//创建内部类 Producer表示生产者线程相关的类
class Producer extends Thread {
@Override
public void run() {
produce();
}
private void produce() {
while (true) {
try {
queue.put(1);
System.out.println("I have inserted one element, the rest capacity is: " + (queueSize - queue.size()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//创建内部类 Consumer表示消费者线程相关的类
class Consumer extends Thread {
@Override
public void run() {
consume();
}
private void consume() {
while (true) {
try {
queue.take();
System.out.println("I have took one element, the rest elements are: " + queue.size());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
输出结果:
.........
I have took one element, the rest elements are: 9
I have took one element, the rest elements are: 8
I have took one element, the rest elements are: 8
I have took one element, the rest elements are: 7
I have took one element, the rest elements are: 6
I have took one element, the rest elements are: 5
I have took one element, the rest elements are: 4
I have took one element, the rest elements are: 3
I have took one element, the rest elements are: 2
I have took one element, the rest elements are: 1
I have took one element, the rest elements are: 0
I have inserted one element, the rest capacity is: 1
I have inserted one element, the rest capacity is: 9
I have inserted one element, the rest capacity is: 9
I have inserted one element, the rest capacity is: 8
I have inserted one element, the rest capacity is: 7
I have inserted one element, the rest capacity is: 6
I have inserted one element, the rest capacity is: 5
I have inserted one element, the rest capacity is: 4
I have inserted one element, the rest capacity is: 3
I have inserted one element, the rest capacity is: 2
I have inserted one element, the rest capacity is: 1
I have inserted one element, the rest capacity is: 0
.........
注意,这两段代码的结果都是截取的部分效果。 读者相比发现了使用阻塞队列代码要简单得多,不需要再单独考虑同步和线程间通信的问题。
在并发编程中,一般推荐使用阻塞队列,这样实现可以尽量地避免程序出现意外的错误。
阻塞队列使用最经典的场景就是socket客户端数据的读取和解析,读取数据的线程不断将数据放入队列,然后解析线程不断从队列取数据解析。还有其他类似的场景,只要符合生产者-消费者模型的都可以使用阻塞队列。
package 并发多线程;
import java.util.concurrent.LinkedBlockingQueue;
/**
* 使用BlockingQueue来实现线程通信
* @author xjh 2019.09.09
* 这里我用了两种玩法:
一种是共享一个queue,根据peek和poll的不同来实现;
第二种是两个queue,利用take()会自动阻塞来实现。
*/
class MethodSeven {
//1.共享一个queue,根据peek和poll的不同来实现;
private final LinkedBlockingQueue queue = new LinkedBlockingQueue<>();
public Runnable newThreadOne() {
final String[] inputArr = Helper.buildNoArr(52);
return new Runnable() {
private String[] arr = inputArr;
public void run() {
for (int i = 0; i < arr.length; i = i + 2) {
Helper.print(arr[i], arr[i + 1]);
queue.offer("TwoToGo");
while (!"OneToGo".equals(queue.peek())) {
}
queue.poll();
}
}
};
}
public Runnable newThreadTwo() {
final String[] inputArr = Helper.buildCharArr(26);
return new Runnable() {
private String[] arr = inputArr;
public void run() {
for (int i = 0; i < arr.length; i++) {
while (!"TwoToGo".equals(queue.peek())) {
}
queue.poll();
Helper.print(arr[i]);
queue.offer("OneToGo");
}
}
};
}
//2.两个queue,利用take()会自动阻塞来实现。
private final LinkedBlockingQueue queue1 = new LinkedBlockingQueue<>();
private final LinkedBlockingQueue queue2 = new LinkedBlockingQueue<>();
public Runnable newThreadThree() {
final String[] inputArr = Helper.buildNoArr(52);
return new Runnable() {
private String[] arr = inputArr;
public void run() {
for (int i = 0; i < arr.length; i = i + 2) {
Helper.print(arr[i], arr[i + 1]);
try {
queue2.put("TwoToGo");
queue1.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
}
public Runnable newThreadFour() {
final String[] inputArr = Helper.buildCharArr(26);
return new Runnable() {
private String[] arr = inputArr;
public void run() {
for (int i = 0; i < arr.length; i++) {
try {
queue2.take();
Helper.print(arr[i]);
queue1.put("OneToGo");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
}
}
//创建一个枚举类型
enum Helper {
instance;
private static final ExecutorService tPool = Executors.newFixedThreadPool(2);
//数字
public static String[] buildNoArr(int max) {
String[] noArr = new String[max];
for(int i=0;i
输出结果:
12A34B56C78D910E1112F1314G1516H1718I1920J2122K2324L2526M2728N2930O3132P3334Q3536R3738S3940T4142U4344V4546W4748X4950Y5152Z
12A34B56C78D910E1112F1314G1516H1718I1920J2122K2324L2526M2728N2930O3132P3334Q3536R3738S3940T4142U4344V4546W4748X4950Y5152Z
参考资料: