热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

深入剖析阻塞队列BlockingQueue(详解ArrayBlockingQueue和LinkedBlockingQueue及其应用)

前言这篇博客南国主要讲解关于Java中阻塞队列的知识点,提到阻

前言

这篇博客南国主要讲解关于Java中阻塞队列的知识点,提到阻塞队列(BlockingQueue)想必大家最先想到的是生产者-消费者,诚然这也是阻塞队列最直接的应用场景。 本篇分为四个章节,BlockingQueue简介,常见的基本操作,常用的BlockingQueue实现类和应用demo。这里针对BlockingQueue的应用南国主要写了生产者-消费者的实现和线程通信的实现。前三个部分的基础知识总结,很多内容参考了并发容器之BlockingQueue的叙述,结合自己的理解南国 在一些内容上做了增加和重新编辑。
话不多说,干货送上~

1. BlockingQueue简介

在实际编程中,会经常使用到JDK中Collection集合框架中的各种容器类如实现List,Map,Queue接口的容器类,但是这些容器类基本上不是线程安全的,除了使用Collections可以将其转换为线程安全的容器,Doug Lea大师为我们都准备了对应的线程安全的容器,如实现List接口的CopyOnWriteArrayList,实现Map接口的ConcurrentHashMap,实现Queue接口的ConcurrentLinkedQueue。

在我们学习操作系统时遇到的一个最经典的"生产者-消费者"问题中,队列通常被视作线程间操作的数据容器,这样,可以对各个模块的业务功能进行解耦,生产者将“生产”出来的数据放置在数据容器中,而消费者仅仅只需要在“数据容器”中进行获取数据即可,这样生产者线程和消费者线程就能够进行解耦,只专注于自己的业务功能即可。阻塞队列(BlockingQueue)被广泛使用在“生产者-消费者”问题中,其原因是BlockingQueue提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止

2. 常见的基本操作

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):当阻塞队列为空时,获取数据的线程会被阻塞,另外,如果被阻塞的线程超过了给定的时长,该线程会退出

3. 常用的BlockingQueue

实现BlockingQueue接口的有ArrayBlockingQueue, DelayQueue, LinkedBlockingDeque, LinkedBlockingQueue, LinkedTransferQueue, PriorityBlockingQueue, SynchronousQueue,而这几种常见的阻塞队列也是在实际编程中会常用的,下面对这几种常见的阻塞队列进行说明:

3.1. ArrayBlockingQueue

ArrayBlockingQueue是由数组实现的有界阻塞队列。该队列命令元素FIFO(先进先出)。因此,对头元素head是队列中存在时间最长的数据元素,而对尾数据tail则是当前队列最新的数据元素。ArrayBlockingQueue可作为“有界数据缓冲区”,生产者插入数据到队列容器中,并由消费者提取。ArrayBlockingQueue一旦创建,容量不能改变。

当队列容量满时,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞。

ArrayBlockingQueue默认情况下不能保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到ArrayBlockingQueue。而非公平性则是指访问ArrayBlockingQueue的顺序不是遵守严格的时间顺序,有可能存在,一旦ArrayBlockingQueue可以被访问时,长时间阻塞的线程依然无法访问到ArrayBlockingQueue。如果保证公平性,通常会降低吞吐量。如果需要获得公平性的ArrayBlockingQueue,可采用如下代码:

private static ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue(10,true);

3.1.1 ArrayBlockingQueue的主要属性

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.await()挂起消费者 非空会去阻塞队列获取数据消费并且调用notFull.signal 告知生产者队列未满,同理 当插入数据的生产者线程发现队列已满时会调用notFull.await()挂起生产者 如果未满则往队列写入元素并调用notEmpty.signal() 通知消费者来消费。而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方法是怎样实现的。

3.1.2 put方法详解

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.await() 挂起线程;,如果当前满足插入数据的条件,就可以直接调用 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())。

3.1.3 take方法详解

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. 如果当前队列为空的话, notEmpty.await(); 挂起当前线程;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就很容易了。

3.2. LinkedBlockingQueue

LinkedBlockingQueue是用链表实现的阻塞队列,同样满足FIFO的特性。与ArrayBlockingQueue相比起来具有更高的吞吐量,为了防止LinkedBlockingQueue容量迅速增,损耗大量内,通常在创建LinkedBlockingQueue对象时,会指定其大小(如果指定了大小,我们判定它为有界队列),如果未指定,容量等于Integer.MAX_VALUE(我们视它为无界队列)。查看它的构造方法:

public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}

3.2.1 LinkedBlockingQueue的主要属性

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方法的实现。

3.2.2 put方法详解

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 notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}

put方法的逻辑也同样很容易理解,可见注释。基本上和ArrayBlockingQueue的put方法一样。

3.2.3 take方法的源码如下:

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方法的主要逻辑请见于注释,也很容易理解。

3.2.4. ArrayBlockingQueue与LinkedBlockingQueue的比较(重要)

相同点:ArrayBlockingQueue和LinkedBlockingQueue都是通过condition通知机制来实现可阻塞式插入和删除元素,并满足线程安全的特性;

不同点:1. ArrayBlockingQueue底层是采用的数组进行实现,而LinkedBlockingQueue则是采用链表数据结构; 2. ArrayBlockingQueue插入和删除数据,只采用了一个lock,而LinkedBlockingQueue则是在插入和删除分别采用了putLock和takeLock,这样可以降低线程由于线程无法获取到lock而进入WAITING状态的可能性,从而提高了线程并发执行的效率。

3.3. PriorityBlockingQueue

PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现compareTo()方法来指定元素排序规则,或者初始化时通过构造器参数Comparator来指定排序规则。

package Concurrent.Blockingqueue;
import lombok.Data;
import java.util.PriorityQueue;
import java.util.concurrent.ThreadLocalRandom;
/**
* @author xiejiahao
* @version 1.0
* @description: PriorityBlockingQueue 简单使用
* 与ArrayBlockingQueue LinkedBlockingQueue二者是有界阻塞队列不同的是,PriorityBlockingQueue是一个无界阻塞队列
* 执行结果说明,任务执行的先后顺序和入堆的先后顺序无关 而是和优先级有关系
* @date 2021/6/13 15:04
*/

public class PriorityQueue_30 {
@Data
static class Task implements Comparable<Task> {
private int priority = 0;
private String taskName;
@Override
public int compareTo(Task o) {
if (this.priority >= o.priority) {
return 1;
} else return -1;
}
private void printTaskAndPriority() {
System.out.println("taskName: " + taskName + ", priority: " + priority);
}
}
public static void main(String[] args) {
PriorityQueue<Task> priorityQueue = new PriorityQueue<>();
for (int i = 0; i < 10; i++) {
Task task = new Task();
task.setPriority(ThreadLocalRandom.current().nextInt(10));
task.setTaskName("taskName: " + i);
priorityQueue.offer(task); //入堆
}
// 取出任务执行
while (!priorityQueue.isEmpty()) {
Task task = priorityQueue.poll(); //取出堆顶元素
if (null != task) {
task.printTaskAndPriority();
}
}
}
}

3.4. SynchronousQueue

它的实质是一种无缓冲的等待队列。SynchronousQueue每个插入操作必须等待另一个线程进行相应的删除操作,因此,SynchronousQueue实际上没有存储任何数据元素,因为只有线程在删除数据时,其他线程才能插入数据,同样的,如果当前有线程在插入数据时,线程才能删除数据。SynchronousQueue也可以通过构造器参数来为其指定公平性。

  • 公平模式:SynchronousQueue采用公平锁,并配合一个FIFO队列来阻塞多余的生产者和消费者。
  • 非公平模式:SynchronoueQueue采用非公平锁,同时配合一个LIFO队列来管理多余的生产者和消费者。

3.5. LinkedTransferQueue

LinkedTransferQueue是一个由链表数据结构构成的无界阻塞队列,由于该队列实现了TransferQueue接口,与其他阻塞队列相比主要有以下不同的方法:

  • transfer(E e) 如果当前有线程(消费者)正在调用take()方法或者可延时的poll()方法进行消费数据时,生产者线程可以调用transfer方法将数据传递给消费者线程。如果当前没有消费者线程的话,生产者线程就会将数据插入到队尾,直到有消费者能够进行消费才能退出;
  • tryTransfer(E e) tryTransfer方法如果当前有消费者线程(调用take方法或者具有超时特性的poll方法)正在消费数据的话,该方法可以将数据立即传送给消费者线程,如果当前没有消费者线程消费数据的话,就立即返回false。因此,与transfer方法相比,transfer方法是必须等到有消费者线程消费数据时,生产者线程才能够返回。而tryTransfer方法能够立即返回结果退出。
  • tryTransfer(E e,long timeout,imeUnit unit)
    与transfer基本功能一样,只是增加了超时特性,如果数据才规定的超时时间内没有消费者进行消费的话,就返回false。

3.6. LinkedBlockingDeque

LinkedBlockingDeque是基于链表数据结构的有界阻塞双端队列,如果在创建对象时为指定大小时,其默认大小为Integer.MAX_VALUE。与LinkedBlockingQueue相比,主要的不同点在于,LinkedBlockingDeque具有双端队列的特性。LinkedBlockingDeque基本操作如下图所示:
在这里插入图片描述
如上图所示,LinkedBlockingDeque的基本操作可以分为四种类型:1.特殊情况,抛出异常;2.特殊情况,返回特殊值如null或者false;3.当线程不满足操作条件时,线程会被阻塞直至条件满足;4. 操作具有超时特性。

另外,LinkedBlockingDeque实现了BlockingDueue接口而LinkedBlockingQueue实现的是BlockingQueue,这两个接口的主要区别如下图所示:
在这里插入图片描述
从上图可以看出,两个接口的功能是可以等价使用的,比如BlockingQueue的add方法和BlockingDeque的addLast方法的功能是一样的。

3.7. DelayQueue

DelayQueue是一个存放实现Delayed接口的数据的无界阻塞队列,只有当数据对象的延时时间达到时才能插入到队列进行存储。如果当前所有的数据都还没有达到创建时所指定的延时期,则队列没有队头,并且线程通过poll等方法获取数据元素则返回null。所谓数据延时期满时,则是通过Delayed接口的getDelay(TimeUnit.NANOSECONDS)来进行判定,如果该方法返回的是小于等于0则说明该数据元素的延时期已满。

package Concurrent.Blockingqueue;
import lombok.Data;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
/**
* @author xiejiahao
* @version 1.0
* @description: DelayQueue队列简单使用
* DelayQueue 内部使用PriorityQueue存放数据,使用ReentrantLock实现线程同步。
* 另外 队列中的元素要实现Delay接口,每个元素都有一个过期时间。
* @date 2021/6/13 16:01
*/

public class DelayQueue_40 {
@Data
static class DelayElement implements Delayed {
// 延迟时间
private long delayTime;
// 到期时间
private long expire;
//任务名称
private String taskName;
DelayElement(long delayTime, String taskName) {
this.delayTime = delayTime;
this.taskName = taskName;
this.expire = System.currentTimeMillis() + this.delayTime;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(this.expire - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
}
}
public static void main(String[] args) {
DelayQueue<DelayElement> delayQueue = new DelayQueue<>();
for (int i = 0; i < 10; i++) {
DelayElement element = new DelayElement(ThreadLocalRandom.current().nextInt(100), "task: " + i);
delayQueue.offer(element);
}
DelayElement element = null;
try {
// for (; ; ) {
while ((element = delayQueue.take()) != null) {
System.out.println(element.toString());
}
// }
} catch (Exception e) {
e.printStackTrace();
}
}
}

4. 应用Demo

通过前面的学习,相比你已经对阻塞队列以及常用的类型有了一个基本的了解。 下面,南国将两个阻塞应用的最广泛的例子:生产者-消费者模式的实现,实现线程通信

4.1 实现生产者-消费者模式

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客户端数据的读取和解析,读取数据的线程不断将数据放入队列,然后解析线程不断从队列取数据解析。还有其他类似的场景,只要符合生产者-消费者模型的都可以使用阻塞队列。

4.2 阻塞队列实现线程通信(这里我使用的是LinkedBlockingQueue)

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 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 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 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 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 noArr[i] = Integer.toString(i+1);
}
return noArr;
}
//字母
public static String[] buildCharArr(int max) {
String[] charArr = new String[max];
int tmp = 65;
for(int i=0;i charArr[i] = String.valueOf((char)(tmp+i));
}
return charArr;
}
public static void print(String... input){
if(input==null)
return;
for(String each:input){
System.out.print(each);
}
}
public void run(Runnable r){
tPool.submit(r);
}
public void shutdown(){
tPool.shutdown();
}
}
public class BlockingQueueTest {
public static void main(String args[]) throws InterruptedException {
MethodSeven seven = new MethodSeven();
Helper.instance.run(seven.newThreadOne());
Helper.instance.run(seven.newThreadTwo());
Thread.sleep(2000);
System.out.println("");
Helper.instance.run(seven.newThreadThree());
Helper.instance.run(seven.newThreadFour());
Helper.instance.shutdown();
}
}

输出结果:

12A34B56C78D910E1112F1314G1516H1718I1920J2122K2324L2526M2728N2930O3132P3334Q3536R3738S3940T4142U4344V4546W4748X4950Y5152Z
12A34B56C78D910E1112F1314G1516H1718I1920J2122K2324L2526M2728N2930O3132P3334Q3536R3738S3940T4142U4344V4546W4748X4950Y5152Z

4.3 logback 异步打印日志中ArrayBlockingQueue的使用

(2021-06更新)
logback的异步日志模型是一个多生产者和单消费者的模型, 其通过使用队列把同步日志打印转换成异步,业务线程只需调用异步appender把日志任务放入队列,而日志线程则负责使用同步的appender进行具体的日志打印。日志打印线程只负责生产日志并将其放入队列,不关心消费线程何时把日志jurisdiction写入磁盘。
其中AsyncAppender 是实现异步日志的关键。
例如,logback.xml配置中出现



0

512



追踪源代码

public class AsyncAppender extends AsyncAppenderBase
...
public class AsyncAppenderBase extends UnsynchronizedAppenderBase implements AppenderAttachable {
AppenderAttachableImpl aai = new AppenderAttachableImpl();
BlockingQueue blockingQueue;
public static final int DEFAULT_QUEUE_SIZE = 256;
int queueSize = 256;
int appenderCount = 0;
static final int UNDEFINED = -1;
int discardingThreshold = -1;
boolean neverBlock = false;
AsyncAppenderBase.Worker worker = new AsyncAppenderBase.Worker();
public static final int DEFAULT_MAX_FLUSH_TIME = 1000;
int maxFlushTime = 1000;
.....
public void start() {
if (!this.isStarted()) {
if (this.appenderCount == 0) {
this.addError("No attached appenders found.");
} else if (this.queueSize <1) {
this.addError("Invalid queue size [" + this.queueSize + "]");
} else {
this.blockingQueue = new ArrayBlockingQueue(this.queueSize);
if (this.discardingThreshold == -1) {
this.discardingThreshold = this.queueSize / 5;
}
this.addInfo("Setting discardingThreshold to " + this.discardingThreshold);
this.worker.setDaemon(true);
this.worker.setName("AsyncAppender-Worker-" + this.getName());
super.start();
this.worker.start();
}
}
}
}

由此可以看到 logback使用的是有界队列ArrayBlockingQueue 之所以设计成为有界队列主要是考虑内存溢出问题。在高并发下写日志的QPS很高,若设置为无界队列,队列自身占用很大的内存,容易出现OOM。

参考资料:

  1. 并发容器之BlockingQueue
  2. Java并发编程:阻塞队列

推荐阅读
author-avatar
贝乐小凸
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有