目录
1、概念
1.1 什么是阻塞队列
从名字上 可以看出,它是队列的一种,那肯定是 先进先出(FIFO)的数据结构
1.2 特性
- 阻塞添加:当阻塞队列是满时,继续 添加的操作将被阻塞,直到被其他线程从队列取走元素
- 阻塞移除:当阻塞队列是空时,继续 获取的操作将被阻塞,直到被其他线程从队列添加元素
在多线程中,多线程之间进行数据交互,可以使用阻塞队列简化代码实现
应用场景:
- 线程池的底层存储
- 生产者消费者模型
下面详细讲解 生产者消费者模型
2、生产者消费者模型
这是一种常用的并发式编程模型,用于解决生产者和消费者之间的数据传输和同步问题
组成:
- 生产者:负责生成数据并将其存储在共享数据结构中(阻塞队列)
- 消费者:负责从共享数据结构中获取并进行处理
- 共享数据结构:一般为阻塞队列实现
生产者往阻塞队列中插入数据,消费者从阻塞队列取出数据,队列的阻塞性可以实现生产者和消费者之间的同步和互斥
生活中的例子 擀饺子皮(生产者)、包饺子(消费者),放饺子皮(阻塞队列)
2.1 解耦合
可以让上下游模块之间,进行更好的“解耦合”
耦合:两个或多组模块之间的依赖关系
问题:高耦合
解决: 引入阻塞队列,作为数据传输点
2.2 削峰填谷
问题:传输的数据、速度直接影响,超出限度可能出现问题
解决:削峰填谷
2.3 模型实现
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> bq = new LinkedBlockingQueue<>();
//消费者
Thread consumer = new Thread(()->{
while (true){
try {
int value = bq.take();
System.out.println("消费元素:"+value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
consumer.start();
//生产者
Thread producer = new Thread(()->{
int value = 0;
while (true){
try {
System.out.println("生产元素:"+value);
bq.put(value);
value++;
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//生产一个元素 消费一个元素
producer.start();
}
3、阻塞队列 方法及实现
3.1 方法
方法类型 | 抛出异常 | 返会布尔 | 阻塞 |
插入 | add() | offer() | put() |
取出 | remove() | poll() | take() |
队首 | element() | peek() | 无 |
注意:
- BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
- put 方法用于阻塞式的入队列, take 用于阻塞式的出队列
- BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.
3.2 核心
阻塞队列核心 两个方法 put() 和 take()
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> bq1 = new LinkedBlockingQueue<>();
//阻塞队列核心 两个方法 put() 和 take()
//put() 阻塞放入
bq1.put(1);
bq1.put(2);
bq1.put(3);
bq1.put(4);
bq1.put(5);
//take() 取出
System.out.println(bq1.take());
System.out.println(bq1.take());
System.out.println(bq1.take());
System.out.println(bq1.take());
System.out.println(bq1.take());
//已经全部取出
//再取 就进入阻塞等待
System.out.println(bq1.take());
}
}
3.3 实现
步骤:
- 先实现一个普通队列
- 加上阻塞功能
- 加上线程安全
3.3.1 普通队列
实现循环 单向队列,取出和存放
class MyBlockingQueue{
//数组 存储数据
private int[] elem = new int[1000];
//头尾
private int head = 0;
private int tail = 0;
//有效数据
private int useSize = 0;
//put 添加
public void put(int value){
//判满
if(useSize == elem.length){
return;
}
//添加 放到尾坐标上
elem[tail] = value;
tail++;
//放到最后 从头开始
if(tail == elem.length){
tail = 0;
}
useSize++;
}
//take 移除
public int put(){
//判空
if(useSize == 0){
return -1;
}
//从头 取出
int value = elem[head];
head++;
//取到尾 从头开始
if(head == elem.length){
head = 0;
}
useSize--;
return value;
}
}
3.3.2 加上阻塞
- 加阻塞 wait 和 notify 前提是 加锁
- 存放 put() ,满就等待 wait,进入阻塞,等待取出,notify唤醒
- 取出 take(),空就等待 wait,进入阻塞,等待存放,notify唤醒
//put 阻塞添加
synchronized public void put(int value) throws InterruptedException {
//判满
if(useSize == elem.length){
//return;
//等待取出
this.wait();
}
//添加 放到尾坐标上
elem[tail] = value;
tail++;
//放到最后 从头开始
if(tail == elem.length){
tail = 0;
}
useSize++;
//唤醒获取等待
this.notify();
}
//take 阻塞移除
synchronized public int put() throws InterruptedException {
//判空
if(useSize == 0){
//return -1;
//等待存放
this.wait();
}
//从头 取出
int value = elem[head];
head++;
//取到尾 从头开始
if(head == elem.length){
head = 0;
}
useSize--;
//唤醒存放等待
this.notify();
return value;
}
- 上述加上 wait() 和 notify() 虽然可以进入等待 和 唤醒,但是还有一个问题,如果不是norify()唤醒的,其他代码中 interrupt 把wait唤醒了,条件不满足但是继续执行了,那么就会出现错误
- 写入操作可能出重排序排序
解决
- 提前唤醒,那就在判断一次条件是否满足,将 if 给为 while
- 给元素加上 volatile
class MyBlockingQueue{
//数组 存储数据
private int[] elem = new int[1000];
//头尾
volatile private int head = 0;
volatile private int tail = 0;
//有效数据
volatile private int useSize = 0;
//put 阻塞添加
synchronized public void put(int value) throws InterruptedException {
//判满
while(useSize == elem.length){
//return;
//等待取出
this.wait();
}
//添加 放到尾坐标上
elem[tail] = value;
tail++;
//放到最后 从头开始
if(tail == elem.length){
tail = 0;
}
useSize++;
//唤醒获取等待
this.notify();
}
//take 阻塞移除
synchronized public Integer take() throws InterruptedException {
//判空
while(useSize == 0){
//return -1;
//等待存放
this.wait();
}
//从头 取出
int value = elem[head];
head++;
//取到尾 从头开始
if(head == elem.length){
head = 0;
}
useSize--;
//唤醒存放等待
this.notify();
return value;
}
}
public static void main(String[] args) {
MyBlockingQueue queue = new MyBlockingQueue();
// 消费者
Thread t1 = new Thread(() -> {
while (true) {
try {
int value = queue.take();
System.out.println("消费: " + value);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 生产者
Thread t2 = new Thread(() -> {
int value = 0;
while (true) {
try {
System.out.println("生产: " + value);
queue.put(value);
value++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}