阻塞队列BlockingQueue
什么是队列
队列是一种遵循先进先出(First-In-First-Out,FIFO)原则的数据结构。简单来说,队列就像是排队等待服务的人群,新来的人总是排在队尾,而最先到达的人则排在队首。
队列有两个基本操作:入队(enqueue)和出队(dequeue)。入队操作将元素添加到队列的末尾,而出队操作则从队列的首部移除元素。这样就保证了先入队的元素会先出队,符合FIFO的规则
阻塞队列-BlockingQueue
简述
阻塞队列(BlockingQueue)是位于 java.util.concurrent 包下,它是一种线程安全的队列,支持在队列为空时阻塞取队列元素的操作,以及在队列满时往阻塞队列中添加元素的操作。阻塞队列常用于生产者和消费者的场景中进行线程间通信。
阻塞队列的特点
阻塞队列的特点如下:
- 线程安全:阻塞队列的所有操作都是线程安全的,可以在多线程环境中直接使用
- 阻塞操作:阻塞队列的取操作和添操作可以在特定条件下进行阻塞。如果队列为空,取 操作会阻塞直到队列不为空;如果队列已满,添 操作会阻塞到直到队列不为满
- 容量限制:阻塞队列的容量通常是固定的,但也可以是动态的。如果是动态的,队列的容量可以根据需要自动扩展或缩小,如ArrayBlockingQueue初始化时需要指定容量,LinkedBlockingQueue则默认上限容量为 Integer.MAX_VALUE,也可以初始化时指定容量。
- 优先级:阻塞队列通常没有提供优先级控制,所有元素都是先进先出(First-In-First-Out,FIFO)的。。但是,一些实现类(如PriorityBlockingQueue)提供了基于优先级的排序功能
- 并发访问:阻塞队列支持多个线程同时进行取和添加操作。
- 失败策略:阻塞队列的操作通常会返回一个结果,如果操作失败,通常抛出异常或返回特殊值
阻塞队列接口的实现
BlockingQueue阻塞队列是属于一个接口,底下有七个实现类
- ArrayBlockQueue:由数组结构组成的有界阻塞队列
- LinkedBlockingQueue:由链表结构组成的有界(但是默认大小 Integer.MAX_VALUE)的阻塞队列
- 有界,但是界限非常大,相当于无界,可以当成无界
- PriorityBlockQueue:支持优先级排序的无界阻塞队列
- DelayQueue:使用优先级队列实现的延迟无界阻塞队列
- SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列
- 生产一个,消费一个,不存储元素,不消费不生产
- LinkedTransferQueue:由链表结构组成的无界阻塞队列
- LinkedBlockingDeque:由链表结构组成的双向阻塞队列。
核心方法
方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) | offer() | put(e) | offer(e,time,unit) |
移除 | remove() | poll() | take() | poll(time,unit) |
检查 | element() | peek | 不可用 | 不可用 |
-
抛出异常:当阻塞队列满时:在队列中add插入元素会抛出 java.lang.IllegalStateException: Queue full ;当阻塞队列空时:再往队列中 remove 元素,会抛出 java.util.NoSuchElementException
-
特殊值:插入方法,成功true,失败false 移除方法:成功返回出队列元素,队列没有就返回空
-
一直阻塞:当阻塞队列满时,生产者继续往队列里 put 元素,队列会一直阻塞生产线程直到 put 数据 或 响应中断退出,当阻塞队列空时,消费者线程试图从队列里 take 元素,队列uhi一直阻塞消费者线程直到队列可用
-
超时退出:当阻塞队列满时,队里会阻塞生产者线程一定时间,超过限时后生产者线程会退出
阻塞队列核心方法使用案例
此处以 ArrayBlockingQueue 为例
抛出异常组
添加元素
执行add方法,向已经满的ArrayBlockingQueue中添加元素时候,会抛出异常
public static void main(String[] args) {
BlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(arrayBlockingQueue.add(1));
System.out.println(arrayBlockingQueue.add(2));
System.out.println(arrayBlockingQueue.add(3));
//在达到容量之后继续添加
System.out.println(arrayBlockingQueue.add(4));
}
执行结果
true
true
true
Exception in thread "main" java.lang.IllegalStateException: Queue full
at java.util.AbstractQueue.add(AbstractQueue.java:98)
at java.util.concurrent.ArrayBlockingQueue.add(ArrayBlockingQueue.java:312)
at com.avgrado.demo.thread.BlockingQueueDemo.main(BlockingQueueDemo.java:19)
取出元素
同时如果我们多取出元素的时候,也会抛出异常,我们只存储了3个值,但是取的时候,取了四次
public static void main(String[] args) {
BlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(arrayBlockingQueue.add(1));
System.out.println(arrayBlockingQueue.add(2));
System.out.println(arrayBlockingQueue.add(3));
System.out.println(arrayBlockingQueue.remove());
System.out.println(arrayBlockingQueue.remove());
System.out.println(arrayBlockingQueue.remove());
//队列为空后继续取出
System.out.println(arrayBlockingQueue.remove());
}
执行结果
true
true
true
1
2
3
Exception in thread "main" java.util.NoSuchElementException
at java.util.AbstractQueue.remove(AbstractQueue.java:117)
at com.avgrado.demo.thread.BlockingQueueDemo.main(BlockingQueueDemo.java:23)
布尔类型组
添加元素使用 offer 方法,如果阻塞队列满了后,会返回false 取出元素使用 poll 方法,如果队列已空,那么会返回null
public static void main(String[] args) {
BlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(arrayBlockingQueue.offer(1));
System.out.println(arrayBlockingQueue.offer(2));
System.out.println(arrayBlockingQueue.offer(3));
//达到容量之后继续添加
System.out.println(arrayBlockingQueue.offer(4));
System.out.println(arrayBlockingQueue.poll());
System.out.println(arrayBlockingQueue.poll());
System.out.println(arrayBlockingQueue.poll());
//队列为空后继续取出
System.out.println(arrayBlockingQueue.poll());
}
执行结果
true
true
true
false
1
2
3
null
阻塞组
使用 put 方法,添加元素时候,如果阻塞队列满了后,添加消息的线程,会一直阻塞,直到队列元素减少到队列容量以下,才会唤醒 使用take 方法取消息的时候,如果阻塞队列为空,获取消息的线程会一直阻塞,直到队列不为空,才会唤醒
添加元素
class QueueDemo{
BlockingQueue blockingQueue = new ArrayBlockingQueue(2);
public void putEle() throws Exception {
System.out.println(Thread.currentThread().getName()+"\t 第一次添加元素");
blockingQueue.put("1");
blockingQueue.put("2");
System.out.println(Thread.currentThread().getName()+"\t =====");
blockingQueue.put("3");
System.out.println(Thread.currentThread().getName()+"\t 第二次添加元素");
}
public void takeEle() throws Exception {
System.out.println(Thread.currentThread().getName()+"\t 准备取出元素");
Object takeValue = blockingQueue.take();
System.out.println(Thread.currentThread().getName()+"\t 取出的元素是"+takeValue);
}
}
/**
* @ClassName BlockingQueueDemo
*/
public class BlockingQueueDemo {
public static void main(String[] args) throws Exception {
QueueDemo queueDemo = new QueueDemo();
new Thread(()->{
try {
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
//睡5秒,保证A线程添加元素全部执行,并能展示阻塞效果
TimeUnit.SECONDS.sleep(5);
new Thread(()->{
try {
queueDemo.takeEle();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
执行结果:
取出元素
class QueueDemo{
BlockingQueue blockingQueue = new ArrayBlockingQueue(2);
public void putEle() throws Exception {
System.out.println(Thread.currentThread().getName()+"\t 添加元素");
blockingQueue.put("1");
System.out.println(Thread.currentThread().getName()+"\t 添加元素完成");
}
public void takeEle() throws Exception {
System.out.println(Thread.currentThread().getName()+"\t 准备取出元素");
Object takeValue = blockingQueue.take();
System.out.println(Thread.currentThread().getName()+"\t 取出的元素是"+takeValue);
}
}
/**
* @ClassName BlockingQueueDemo
*/
public class BlockingQueueDemo {
public static void main(String[] args) throws Exception {
QueueDemo queueDemo = new QueueDemo();
new Thread(()->{
try {
queueDemo.takeEle();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
//睡5秒,保证A线程先执行取出元素,并能展示阻塞效果
TimeUnit.SECONDS.sleep(5);
new Thread(()->{
try {
queueDemo.putEle();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
}
执行结果:
超时时间组
offer() 和 poll() 方法加上时间参数
使用offer插入的时候,需要指定时间,如果2秒还没有插入,那么就放弃插入: 使用poll取出的时候,需要指定时间,如果2秒还没有取出,那么就放弃取出
public static void main(String[] args) throws Exception {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.offer("a", 2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.offer("b", 2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.offer("c", 2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.offer("d", 2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
}
执行结果
true
true
true
false
a
b
c
null
不存储元素的 SynchronousQueue
SynchronousQueue没有容量,与其他BlockingQueue不同,SynchronousQueue是一个不存储的BlockingQueue,每一个put操作必须等待一个take操作,否者不能继续添加元素
代码示例:
public static void main(String[] args) {
BlockingQueue<String> synchronousQueue = new SynchronousQueue<>();
new Thread(()->{
try{
System.out.println(Thread.currentThread().getName()+"\t put A");
synchronousQueue.put("A");
System.out.println(Thread.currentThread().getName()+"\t put B");
synchronousQueue.put("B");
System.out.println(Thread.currentThread().getName()+"\t put C");
synchronousQueue.put("C");
}catch (Exception e){
e.printStackTrace();
}
},"t1").start();
new Thread(() -> {
try {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronousQueue.take();
System.out.println(Thread.currentThread().getName() + "\t take A ");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronousQueue.take();
System.out.println(Thread.currentThread().getName() + "\t take B ");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronousQueue.take();
System.out.println(Thread.currentThread().getName() + "\t take C ");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2").start();
}
执行结果:
t1 put A
5秒后...
t2 take A
t1 put B
5秒后...
t2 take B
t1 put C
5秒后...
t2 take C
最后的运行结果可以看出,每次t1线程向队列中添加阻塞队列添加元素后,t1输入线程就会等待 t2消费线程,t2消费后,t2处于挂起状态,等待t1在存入,从而周而复始,形成 一存一取的状态
阻塞队列的用处
生产者消费者模式
1.使用传统的 Lock 实现简易版本的生产者-消费者模式 :对一个初始值为0的变量,两个线程对其交替操作,一个加1,一个减1,来5轮
/**
* 定义资源类
*/
class ShareData{
private int num = 0;
private Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
/**
* 自增:生产
* @throws Exception
*/
public void increment() throws Exception{
lock.lock();
try{
while (num !=0){
condition.await();
}
num ++;
System.out.println(Thread.currentThread().getName()+"\t "+num);
//通知唤醒
condition.signalAll();
}finally {
lock.unlock();
}
}
/**
* 自减:消费
* @throws Exception
*/
public void decrement() throws Exception{
lock.lock();
try{
while (num ==0){
condition.await();
}
num --;
System.out.println(Thread.currentThread().getName()+"\t "+num);
//通知唤醒
condition.signalAll();
}finally {
lock.unlock();
}
}
}
/**
* @ClassName ProdConsumerTraditionDemo
* @Description TODO
* @Author gongchen
* @Date 2023-11-20 11:15
*/
public class ProdConsumerTraditionDemo {
public static void main(String[] args) {
ShareData shareData = new ShareData();
// t1线程,生产
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
shareData.increment();
} catch (Exception e) {
e.printStackTrace();
}
}
}, "生产线程:t1").start();
// t2线程,消费
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
shareData.decrement();
} catch (Exception e) {
e.printStackTrace();
}
}
}, "消费线程:t2").start();
}
}
执行结果:
生产线程:t1 1
消费线程:t2 0
生产线程:t1 1
消费线程:t2 0
生产线程:t1 1
消费线程:t2 0
生产线程:t1 1
消费线程:t2 0
生产线程:t1 1
消费线程:t2 0
使用:volatile、CAS、atomicInteger、BlockQueue、线程交互、原子引用 完成阻塞队列版生产者和消费者示例
class MyResoure{
// 默认开启,进行生产消费
// 这里用到了volatile是为了保持数据的可见性,也就是当 FLAG 修改时,要马上通知其它线程进行修改
private volatile Boolean FLAG = true;
//使用原子包装类
private AtomicInteger atomicInteger = new AtomicInteger();
//定义一个 未实现的 BlockingQueue ,在构造方法中从传入的实现类中进行实例化
BlockingQueue<String> blockingQueue = null;
//而应该采用依赖注入里面的,构造注入方法传入
public MyResoure (BlockingQueue<String> blockingQueue){
this.blockingQueue = blockingQueue;
// 查询出传入的class是什么
System.out.println(Thread.currentThread().getName());
}
/**
* 生产
* @throws Exception
*/
public void produce() throws Exception{
String data = "";
boolean retValue ;
// 多线程环境的判断,一定要使用while进行,防止出现虚假唤醒
// 当FLAG为true的时候,开始生产
while (FLAG){
data= atomicInteger.incrementAndGet()+"";
retValue = blockingQueue.offer(data,2L, TimeUnit.SECONDS);
if(retValue){
System.out.println(Thread.currentThread().getName()+"\t 插入队列"+ data + "成功");
}else{
System.out.println(Thread.currentThread().getName()+"\t 插入队列"+ data + "失败");
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "\t 停止生产,表示FLAG=false,生产结束");
}
/**
* 消费
* @throws Exception
*/
public void consume() throws Exception{
String retValue ;
// 多线程环境的判断,一定要使用while进行,防止出现虚假唤醒
// 当FLAG为true的时候,开始生产
while (FLAG){
retValue = blockingQueue.poll(2L,TimeUnit.SECONDS);
if(retValue!=null && retValue != ""){
System.out.println(Thread.currentThread().getName()+"\t 消费队列:"+retValue +"成功");
}else{
FLAG = false;
System.out.println(Thread.currentThread().getName()+"\t 消费失败,队列中已为空,退出");
//退出消费队列
return;
}
}
}
/**
* 停止生产的判断
*/
public void stop() {
this.FLAG = false;
}
}
/**
* @ClassName ProdConsumerTraditionDemo
* @Description TODO
* @Author gongchen
* @Date 2023-11-20 11:15
*/
public class ProdConsumerTraditionDemo {
public static void main(String[] args) {
//传入具体的实现类: ArrayBlockingQueue
MyResoure resoure = new MyResoure(new ArrayBlockingQueue<>(10));
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "\t 生产线程启动");
try {
resoure.produce();
} catch (Exception e) {
e.printStackTrace();
}
},"生产者T1").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "\t 消费线程启动");
try {
resoure.consume();
} catch (Exception e) {
e.printStackTrace();
}
},"消费者T2").start();
// 5秒后,停止生产和消费
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("");
System.out.println("");
System.out.println("5秒中后,生产和消费线程停止,线程结束");
resoure.stop();
}
}