系列文章目录
Java并发编程技术知识点梳理(第一篇)操作系统底层工作的整体认识
Java并发编程技术知识点梳理(第二篇)并发编程之JMM&volatile详解
Java并发编程技术知识点梳理(第三篇)CPU缓存一致性协议MESI
Java并发编程技术知识点梳理(第四篇)并发编程之synchronized详解
Java并发编程技术知识点梳理(第五篇)抽象队列同步器AQS应用Lock详解
Java并发编程技术知识点梳理(第六篇)并发编程之LockSupport的 park 方法及线程中断响应
Java并发编程技术知识点梳理(第七篇)抽象队列同步器AQS应用之阻塞队列BlockingQueue详解
Java并发编程技术知识点梳理(第八篇)并发编程之CountDownLatch&Semaphore原理与应用
Java并发编程技术知识点梳理(第九篇)并发编程之Atomic&Unsafe魔法类详解
Java并发编程技术知识点梳理(第十篇)Collections之HashMap分析
Java并发编程技术知识点梳理(第十一篇)并发编程之Executor线程池原理与源码解读
Java并发编程技术知识点梳理(第十二篇)并发编程之定时任务&定时线程池
抽象队列同步器AQS应用之阻塞队列BlockingQueue详解
概要
BlockingQueue,是java.util.concurrent 包提供的用于解决并发生产者 - 消费者问题的最有用的类,它的特性是在任意时刻只有一个线程可以进行take或者put操作,并且BlockingQueue提供了超时return null的机制,在许多生产场景里都可以看到这个工具的身影。
队列类型
无限队列 (unbounded queue ) - 几乎可以无限增长
有限队列 ( bounded queue ) - 定义了最大容量
队列数据结构
队列实质就是一种存储数据的结构
- 通常用链表或者数组实现
- 一般而言队列具备FIFO先进先出的特性,当然也有双端队列(Deque)优先级队列
- 主要操作:入队(EnQueue)与出队(Dequeue)
常见的4种阻塞队列
- ArrayBlockingQueue 由数组支持的有界队列
- LinkedBlockingQueue 由链接节点支持的可选有界队列
- PriorityBlockingQueue 由优先级堆支持的无界优先级队列
- DelayQueue 由优先级堆支持的、基于时间的调度队列
ArrayBlockingQueue
队列基于数组实现,容量大小在创建ArrayBlockingQueue对象时已定义好
队列创建:
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(10);
应用场景
在线程池中有比较多的应用,生产者消费者场景
工作原理
基于ReentrantLock保证线程安全,根据Condition实现队列满时的阻塞
LinkedBlockingQueue
是一个基于链表的无界队列(理论上有界)
BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>();
上面这段代码中,blockingQueue 的容量将设置为 Integer.MAX_VALUE 。
向无限队列添加元素的所有操作都将永远不会阻塞,因此它可以增长到非常大的容量。
使用无限 BlockingQueue 设计生产者 - 消费者模型时最重要的是 消费者应该能够像生产者向队列添加消息一样快地消费消息 。否则,内存可能会填满,然后就会得到一个 OutOfMemory 异常。
DelayQueue
由优先级堆支持的、基于时间的调度队列,内部基于无界队列PriorityQueue实现,而无界队列基于数组的扩容实现。
BlockingQueue<String> blockingQueue = new DelayQueue();
要求
入队的对象必须要实现Delayed接口,而Delayed集成自Comparable接口
应用场景
电影票
工作原理:
队列内部会根据时间优先级进行排序。延迟类线程池周期执行。
BlockingQueue API
BlockingQueue 接口的所有方法可以分为两大类:负责向队列添加元素的方法和检索这些元素的方法。在队列满/空的情况下,来自这两个组的每个方法的行为都不同。
添加元素
方法 | 说明 |
---|---|
add() | 如果插入成功则返回 true,否则抛出 IllegalStateException 异常 |
put() | 将指定的元素插入队列,如果队列满了,那么会阻塞直到有空间插入 |
offer() | 如果插入成功则返回 true,否则返回 false |
offer(E e, long timeout, TimeUnit unit) | 尝试将元素插入队列,如果队列已满,那么会阻塞直到有空间插入 |
检索元素
方法 | 说明 |
---|---|
take() | 获取队列的头部元素并将其删除,如果队列为空,则阻塞并等待元素变为可用 |
poll(long timeout, TimeUnit unit) | 检索并删除队列的头部,如有必要,等待指定的等待时间以使元素可用,如果超时,则返回 null |
多线程生产者-消费者示例
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
- 解耦
- 支持并发(concurrency)
- 支持忙闲不均
package com.yg.edu.queue.test;
/**
* @author 史凯强
* @date 2021/10/27 13:31
* @desc
**/
public class Main {
public static void main(String[] args) {
Resource resource = new Resource();
//生产者线程
ProducerThread p1 = new ProducerThread(resource,"生产者1");
ProducerThread p2 = new ProducerThread(resource,"生产者2");
ProducerThread p3 = new ProducerThread(resource,"生产者3");
ProducerThread p4 = new ProducerThread(resource,"生产者4");
ProducerThread p5 = new ProducerThread(resource,"生产者5");
//多个消费者
ConsumerThread c1 = new ConsumerThread(resource,"消费者1");
ConsumerThread c2 = new ConsumerThread(resource,"消费者2");
ConsumerThread c3 = new ConsumerThread(resource,"消费者3");
p1.start();
p2.start();
p3.start();
p4.start();
p5.start();
c1.start();
c2.start();
c3.start();
}
}
package com.yg.edu.queue.test;
/**
* @author 史凯强
* @date 2021/10/27 13:32
* @desc
**/
public class ConsumerThread extends Thread{
private Resource resource;
public ConsumerThread(Resource resource,String threadName){
this.resource = resource;
setName(threadName);
}
@Override
public void run() {
while (true) {
try {
Thread.sleep((long) (1000 * Math.random()));
} catch (InterruptedException e) {
e.printStackTrace();
}
resource.remove();
}
}
}
package com.yg.edu.queue.test;
/**
* @author 史凯强
* @date 2021/10/27 13:31
* @desc
**/
public class ProducerThread extends Thread{
private Resource resource;
public ProducerThread(Resource resource,String threadName){
this.resource = resource;
setName(threadName);
}
@Override
public void run() {
while (true) {
try {
Thread.sleep((long) (1000 * Math.random()));
} catch (InterruptedException e) {
e.printStackTrace();
}
resource.add();
}
}
}
package com.yg.edu.queue.test;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* @author 史凯强
* @date 2021/10/27 13:32
* @desc
**/
public class Resource {
private BlockingQueue resourceQueue = new LinkedBlockingQueue(10);
public void remove() {
try {
resourceQueue.take();
System.out.println(Thread.currentThread().getName() +
"消耗一件资源," + "当前资源池有" + resourceQueue.size()
+ "个资源");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void add() {
try {
resourceQueue.put(1);
System.out.println(Thread.currentThread().getName()
+ "生产一件资源," + "当前资源池有" + resourceQueue.size() +
"个资源");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
ArrayBlockingQueue源码解析
对于ArrayBlockingQueue需要掌握以下几点
- 创建
- 入队(添加元素)
- 出队(删除元素)
创建
public ArrayBlockingQueue(int capacity, boolean fair)
public ArrayBlockingQueue(int capacity)
使用方式
Queue<String> abq = new ArrayBlockingQueue<String>(2);
Queue<String> abq = new ArrayBlockingQueue<String>(2,true);
通过使用方法,可以看出ArrayBlockingQueue 支持ReentrantLock的公平锁模式与非公平锁模式。
//底层数据结构
final Object[] items;
//用来为下一个take/poll/remove的索引(出队)
int takeIndex;
//用来为下一个put/offer/add的索引(入队)
int putIndex;
//队列中元素的个数
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;//等待入队的条件
/**
* 创造一个队列,指定队列容量,指定模式
* @param fair
* true:先来的线程先操作
* false:顺序随机
*/
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];//初始化类变量数组items
lock = new ReentrantLock(fair);//初始化类变量锁lock
notEmpty = lock.newCondition();//初始化类变量notEmpty Condition
notFull = lock.newCondition();//初始化类变量notFull Condition
}
/**
* 创造一个队列,指定队列容量,默认模式为非公平模式
* @param capacity <1会抛异常
*/
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
注意:
- ArrayBlockingQueue的组成:一个对象数组+1把锁ReentrantLock+2个条件Condition
- 在查看源码的过程中,也要模仿带条件锁的使用,这个双条件锁模式是很经典的模式
入队
public boolean offer(E e)
原理:
在队尾插入一个元素, 如果队列没满,立即返回true; 如果队列满了,立即返回false
使用方法:
abq.offer(“hello1”);
/**
* 在队尾插入一个元素,
* 如果队列没满,立即返回true;
* 如果队列满了,立即返回false
* 注意:该方法通常优于add(),因为add()失败直接抛异常
*/
public boolean offer(E e) {
checkNotNull(e);
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) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;//插入元素
if (++putIndex == items.length)
putIndex = 0;
count++;//元素数量+1
/**
* 唤醒一个线程
* 如果有任意一个线程正在等待这个条件,那么选中其中的一个区唤醒。
* 在从等待状态被唤醒之前,被选中的线程必须重新获得锁
*/
notEmpty.signal();
}
代码非常简单,流程看注释即可,只有一点注意点:
在插入元素结束后,唤醒等待notEmpty条件(即获取元素)的线程,可以发现这类似于生产者-消费者模式
public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException
原理:
在队尾插入一个元素,,如果数组已满,则进入等待,直到出现以下三种情况:
- 被唤醒
- 等待时间超时
- 当前线程被中断
使用方法
try {
abq.offer("hello2",1000,TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 在队尾插入一个元素,
* 如果数组已满,则进入等待,直到出现以下三种情况:
* 1、被唤醒
* 2、等待时间超时
* 3、当前线程被中断
*/
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
checkNotNull(e);
long nanos = unit.toNanos(timeout);//将超时时间转换为纳秒
final ReentrantLock lock = this.lock;
/*
* lockInterruptibly():
* 1、 在当前线程没有被中断的情况下获取锁。
* 2、如果获取成功,方法结束。
* 3、如果锁无法获取,当前线程被阻塞,直到下面情况发生:
* 1)当前线程(被唤醒后)成功获取锁
* 2)当前线程被其他线程中断
*
* lock()
* 获取锁,如果锁无法获取,当前线程被阻塞,直到锁可以获取并获取成功为止。
*/
lock.lockInterruptibly();//加可中断的锁
try {
while (count == items.length) {
//队列已满
if (nanos <= 0)//已超时
return false;
/*
* 进行等待:
* 在这个过程中可能发生三件事:
* 1、被唤醒-->继续当前这个for(;;)循环
* 2、超时-->继续当前这个for(;;)循环
* 3、被中断-->之后直接执行catch部分的代码
*/
nanos = notFull.awaitNanos(nanos);//进行等待(在此过程中,时间会流失,在此过程中,线程也可能被唤醒)
}
enqueue(e);
return true;
} finally {
lock.unlock();
}
}
注意:
- awaitNanos(nanos)是AQS中的一个方法,这里就不详细说了,有兴趣的自己去查看AQS的源代码。
- lockInterruptibly()与lock()的区别见注释
public void put(E e) throws InterruptedException
原理:
在队尾插入一个元素,如果队列满了,一直阻塞,直到数组不满了或者线程被中断
try {
abq.put("hello1");
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 在队尾插入一个元素
* 如果队列满了,一直阻塞,直到数组不满了或者线程被中断
*/
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)//队列满了,一直阻塞在这里
/*
* 一直等待条件notFull,即被其他线程唤醒
* (唤醒其实就是,有线程将一个元素出队了,然后调用notFull.signal()唤醒其他等待这个条件的线程,同时队列也不慢了)
*/
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
出队
public E poll()
原理:
如果没有元素,直接返回null;如果有元素,将队头元素置null,但是要注意队头是随时变化的,并非一直是items[0]。
使用方法:
abq.poll();
/**
* 出队
*/
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : dequeue();//如果没有元素,直接返回null,而非抛出异常
} finally {
lock.unlock();
}
}
/**
* 出队
*/
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;//将出队元素位置置空
/*
* 第一次出队的元素takeIndex==0,第二次出队的元素takeIndex==1
* (注意:这里出队之后,并没有将后面的数组元素向前移)
*/
if (++takeIndex == items.length)
takeIndex = 0;
count--;//数组元素个数-1
if (itrs != null)
itrs.elementDequeued();
notFull.signal();//数组已经不满了,唤醒其他等待notFull条件的线程
return x;//返回出队的元素
}
public E poll(long timeout, TimeUnit unit) throws InterruptedException
原理:
从队头删除一个元素,如果数组不空,出队;如果数组已空且已经超时,返回null;如果数组已空且时间未超时,则进入等待,直到出现以下三种情况:
- 被唤醒
- 等待时间超时
- 当前线程被中断
try {
abq.poll(1000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 从对头删除一个元素,
* 如果数组不空,出队;
* 如果数组已空,判断时间是否超时,如果已经超时,返回null
* 如果数组已空且时间未超时,则进入等待,直到出现以下三种情况:
* 1、被唤醒
* 2、等待时间超时
* 3、当前线程被中断
*/
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);//将时间转换为纳秒
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0) {
if (nanos <= 0)//时间超时
return null;
/*
* 进行等待:
* 在这个过程中可能发生三件事:
* 1、被唤醒-->继续当前这个for(;;)循环
* 2、超时-->继续当前这个for(;;)循环
* 3、被中断-->之后直接执行catch部分的代码
*/
nanos = notEmpty.awaitNanos(nanos);
}
return dequeue();//数组不空//出队
} finally {
lock.unlock();
}
}
public E take() throws InterruptedException
原理:
将队头元素出队,如果队列空了,一直阻塞,直到数组不为空或者线程被中断
try {
abq.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 将队头元素出队
* 如果队列空了,一直阻塞,直到数组不为空或者线程被中断
*/
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)//如果数组为空,一直阻塞在这里
/*
* 一直等待条件notEmpty,即被其他线程唤醒
* (唤醒其实就是,有线程将一个元素入队了,然后调用notEmpty.signal()唤醒其他等待这个条件的线程,同时队列也不空了)
*/
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
三种入队对比
offer(E e):如果队列没满,立即返回true; 如果队列满了,立即返回false–>不阻塞
put(E e):如果队列满了,一直阻塞,直到数组不满了或者线程被中断–>阻塞
offer(E e, long timeout, TimeUnit unit):在队尾插入一个元素,,如果数组已满,则进入等待,直到出现以下三种情况:–>阻塞
被唤醒
等待时间超时
当前线程被中断
三种出队对比
poll():如果没有元素,直接返回null;如果有元素,出队
take():如果队列空了,一直阻塞,直到数组不为空或者线程被中断–>阻塞
poll(long timeout, TimeUnit unit):如果数组不空,出队;如果数组已空且已经超时,返回null;如果数组已空且时间未超时,则进入等待,直到出现以下三种情况:
被唤醒
等待时间超时
当前线程被中断