目录
1.前言
2.正文
2.1阻塞队列引入
2.2标准库中的阻塞队列
2.3手搓阻塞队列
3.小结
1.前言
哈喽大家好吖,今天来给大家分享多线程学习中的阻塞队列,阻塞队列在我们开发中也是一个非常重要的内容,那么话不多说让我们开始吧。
2.正文
2.1阻塞队列引入
Java中的阻塞队列(Blocking Queue)是一种支持线程安全的队列,主要用于多线程编程中实现生产者-消费者模式。其核心特性是:当队列为空时,消费者线程会被阻塞,直到队列中有新元素;当队列满时,生产者线程会被阻塞,直到队列有空闲位置。这种机制能有效协调生产者和消费者的速度差异。
那我们为什么引入阻塞队列呢。阻塞队列一个应用场景,就是实现生产者消费者模型,在多线程编程中,这是一个典型的编码技巧。接下来讲解我们为什么要引入阻塞队列。
假设我们有这样一个场景:
两台服务器之间互相通信,如果是二者直接进行请求响应的话,二者本身代码肯定存储了部分对方的信息方便通信,这样耦合程度较高。
但如果过我们引入阻塞队列:
这样就是本来是A 和 B耦合,现在成了A和队列耦合,B 和队列耦合,降低耦合, 是为了让后续修改的时候,成本低。
在实际开放中,中间的这个阻塞队列甚至会被单独部署成一个服务(其中包含多个独立的阻塞队列,被称为消息队列)。
所以引入阻塞队列的一个原因就是“降低耦合程度”。
引入阻塞队列还有另一个原因,在引入一个场景:
A 这边遇到一波流量激增,此时每个请求都会转发给 B,B也会承担一样的压力~。很容易就把 B给搞挂了。
一般来说,A这种上游的服务器,尤其是入口的服务器,干的活更简单,单个请求消耗的资源数少。
像B这种下游的服务器,通常承担更重的任务量,复杂的计算/存储工作,单个请求消耗的资源数更多。
日常工作中,确实是会给B这样角色的服务器分配更好的机器,即使如此,也很难保证B承担的访问量能够比A更高。
这个时候我们在AB两者中添加一个阻塞队列:
队列服务器,针对单个请求,做的事情少,往往就比较耐造,a一旦激增,b可以不关心队列中的数据量有多少,可以慢慢的处理阻塞队列的请求数据,起到了“削峰填谷”的作用。
但显然,这样操作也会付出一定的代价:
- 引入队列之后,整体结构会变得复杂,此时部署生产环境,管理起来会更麻烦
- 效率也会受到影响(毕竟有可能出现阻塞)
2.2标准库中的阻塞队列
官网讲解:BlockingQueue (Java SE 17 & JDK 17)
以下代码通过 阻塞队列 (ArrayBlockingQueue
) 实现了一个典型的生产者-消费者模型,展示了多线程间如何安全协作。其核心逻辑可分为以下部分:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;public class demo4 {public static void main(String[] args) {BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);Thread t1 = new Thread(()->{int n = 0;while(true){try {queue.put(n);System.out.println("生产者" + n);n++;Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}}});Thread t2 = new Thread(()->{while(true){try {int x = queue.take();System.out.println("消费者" + x);Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}}});t1.start();t2.start();}
}
1. 阻塞队列的初始化
作用:创建容量为10的阻塞队列,用于在生产者和消费者线程间传递数据。
特性:
当队列满时,
put()
操作会阻塞生产者线程。当队列空时,
take()
操作会阻塞消费者线程。线程安全,无需额外同步。
2. 生产者线程逻辑
行为:
无限循环生成递增整数
n
。调用
put(n)
将数据存入队列。若队列已满,生产者线程自动阻塞。每次生产后睡眠1毫秒,模拟实际生产场景中的处理延迟。
3. 消费者线程逻辑
行为:
无限循环从队列中取出数据。
调用
take()
获取数据。若队列为空,消费者线程自动阻塞。每次消费后睡眠1毫秒,模拟实际消费场景中的处理延迟。
4. 线程启动与协作
作用:启动生产者和消费者线程,二者并行执行。
协作机制:
生产者通过
put()
填充队列,消费者通过take()
清空队列。阻塞队列自动协调两者的速度差异:生产者过快时队列满导致阻塞,消费者过快时队列空导致阻塞。
运行截图:
2.3手搓阻塞队列
为了更好地理解阻塞队列,我们可以不利用标准库自带的阻塞队列,我们手搓一个阻塞队列,这样可加深我们对阻塞队列的理解。
class MyBlockQueue{private String[] data = null;private int head = 0;private int tail = 0;private int size = 0;public MyBlockQueue(int number){data = new String[number];}public void put(String elem) throws InterruptedException{synchronized (this){while(size >= data.length){this.wait();}data[tail] = elem;tail++;if(tail >= data.length){tail = 0;}size++;this.notify();}}public String take() throws InterruptedException{synchronized (this) {while (size == 0) {this.wait();}String ret = data[head];head++;if (head >= data.length) {head = 0;}size--;this.notify();return ret;}}
}public class demo5 {public static int n = 0;public static void main(String[] args) {MyBlockQueue myBlockQueue = new MyBlockQueue(10);Thread t1 = new Thread(()->{while(true){try {myBlockQueue.put(n + "");System.out.println("生产元素" + n);n++;Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});Thread t2 = new Thread(()->{while(true) {try {String str = myBlockQueue.take();System.out.println("消费元素" + str);Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t1.start();t2.start();}
}
核心思路分析:
1. 数据结构与初始化
环形数组:使用定长数组
data
存储元素,通过head
和tail
指针实现环形队列,避免频繁扩容。状态变量:
size
记录当前队列元素数量,用于判断队列空/满。初始化:构造函数指定队列容量(
new MyBlockQueue(10)
),创建固定大小的数组。
2. 线程安全与同步机制
synchronized 锁:
put
和take
方法通过synchronized(this)
实现互斥访问,确保同一时间只有一个线程操作队列。条件等待与唤醒:
生产者等待条件:队列满时(
size >= data.length
),调用wait()
阻塞生产者。消费者等待条件:队列空时(
size == 0
),调用wait()
阻塞消费者。唤醒机制:每次插入/取出元素后调用
notify()
,唤醒一个等待线程(可能是生产者或消费者)。
3. put() 方法详解
阻塞逻辑:
使用
while
而非if
避免虚假唤醒(如线程被意外唤醒但条件未满足)。添加元素:
元素放入
tail
位置,tail
指针循环后移(tail = (tail + 1) % data.length
的等价实现)。
size++
更新队列元素数量。唤醒消费者:
notify()
通知可能阻塞的消费者线程。
4. take() 方法详解
阻塞逻辑:
while (size == 0) { this.wait(); } // 队列空时等待
取出元素:
从
head
位置取元素,head
指针循环后移。
size--
更新队列元素数量。唤醒生产者:
notify()
通知可能阻塞的生产者线程。
5. 生产者-消费者模型演示
生产者线程 (
t1
):
不断生成元素(
n++
),调用put()
插入队列。每次生产后休眠 1 秒,模拟生产耗时。
消费者线程 (
t2
):
不断调用
take()
取出元素并打印。每次消费后休眠 1 秒,模拟处理耗时。
运行效果:
生产和消费速率相同(1秒/次),队列不会满或空。
若生产快于消费,队列满时生产者阻塞;反之消费者阻塞。
运行截图:
3.小结
今天的分享到这里就结束了,喜欢的小伙伴点点赞点点关注,你的支持就是对我最大的鼓励,大家加油!