🌲阻塞队列是什么
阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.
阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:
- 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
- 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.
阻塞队列的一个典型应用场景就是 “生产者消费者模型”. 这是一种非常典型的开发模型
🌳生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
那什么是耦合呢?
🚩耦合
耦合是两个或多个模块之间的相互关联。在软件工程中,两个模块之间的耦合度越高,维护成本越高。因此,在系统架构的设计过程中,应减少各个模块之间的耦合度,以提高应用的可维护性
耦合又分为紧耦合(强耦合)和 松耦合
📌紧耦合(强耦合)
紧耦合架构本质是Client/Server的模型,如下图所示
优点是:架构简单、设计简单、开发周期短、能够快速的开发、投入、部署、应用。
但随着集群规模的扩大,系统的稳定性逐渐变差,主要原因如下:
- 同步操作导致对网络资源消耗大。同步操作在数据发送和数据返回之间,有很大一段是空闲的,这种空闲占用是对网络资源的极大浪费。
- 安全控制力度差,因为服务器直接暴露给客户机,容易引发网络攻击行为。
- 程序代码之间关联度过高,不利于模块化处理。
📌松耦合(解耦合)
松耦合架构本质上是在client/server模型之间加入一个代理,把CS模型变成CAS模型。 在新的架构下,客户机的角色不变,代理服务器承担起与客户机的通信,和对客户机的识别判断工作,服务器位于代理服务器后面,对客户机来说不可见,它只负责数据处理工作,另外我们也把CS模型的同步操作改为CAS的代理处理。 如下图所示:
优点如下:
- 多任务并行处理能力获得极大提升。
- 实现负载自适应机制(根据当时运行环境,松耦合架构分配并行工作任务,避免超载现象)。
- 基本杜绝了对Server服务端的网络攻击行为,由于代理服务器的隔绝和筛查作用, 同时结合其它安全管理手段,外部攻击在代理服务器处就被识别和过滤掉了,这样就保护了后面的服务器不受影响。
- 异步操作减少了网络资源消耗和操作关联。
- 提高了系统的可维护性。
了解了耦合之后,我们就可以通过一个阻塞队列来实现一个生产者消费者的模型
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取
在这个模型当中
- 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力.
比如在 “秒杀” 场景下, 服务器同一时刻可能会收到大量的支付请求. 如果直接处理这些支付请求,服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程). 这个时候就可以把这些请求都放到一个阻塞队列中, 然后再由消费者线程慢慢的来处理每个支付请求.这样做可以有效进行 “削峰”, 防止服务器被突然到来的一波请求直接冲垮
削峰的作用在实现负载自适应机制(根据当时运行环境,松耦合架构分配并行工作任务,避免超载现象)。
- 阻塞队列也能使生产者和消费者之间 解耦.
比如过年一家人一起包饺子. 一般都是有明确分工, 比如一个人负责擀饺子皮, 其他人负责包. 擀饺子皮的人就是 “生产者”, 包饺子的人就是 “消费者”.擀饺子皮的人不关心包饺子的人是谁(能包就行, 无论是手工包, 借助工具, 还是机器包), 包饺子的人也不关心擀饺子皮的人是谁(有饺子皮就行, 无论是用擀面杖擀的, 还是拿罐头瓶擀, 还是直接从超市买的).
🎄Java标准库中的阻塞队列的使用
在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可
使用注意事项:
- BlockingQueue 是一个接口. 真正实现的类是有以下几种
- put 方法用于阻塞式的入队列, take 用于阻塞式的出队列
- BlockingQueue 也有== offer, poll, peek 等方法, 但是这些方法不带有阻塞特性==
BlockingQueue<String> queue = new LinkedBlockingQueue<>(); // 入队列 queue.put("abc"); // 出队列. 如果没有 put 直接 take, 就会阻塞. String elem = queue.take();
🚩标准库实现消费者生产者模型
这个实现比较简单,这里就直接上代码了
public class ThreadDemo1 { public static void main(String[] args) { BlockingDeque<Integer> blockingQueue = new LinkedBlockingDeque<>(); Thread customer = new Thread(() -> { while(true) { try { int a = blockingQueue.take(); System.out.println("消费元素为:" + a); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); Thread producer = new Thread(() -> { int n = 0; while(true) { try { System.out.println("生产元素为:" + n); blockingQueue.put(n++); Thread.sleep(500); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); customer.start(); producer.start(); } }
运行结果如下:
我们可以看到基本上是成对出现的,生产一个,消费一个。趋于稳定
接下来我们自己模拟实现一个阻塞队列
🍀阻塞队列的模拟实现
这里我们又两种结构可以选择,一种是链表的,另一种是数组的形式实现
这里博主选择的是用数组的形式进行模拟实现
首先我们创建一个 “循环队列” ,关于循环队列了解的小伙伴,可以去看看博主相应的博客【数据结构】详解环形队列
这里只展示一个循环队列的代码:
public class MyBlockingQueue { private int[] items = new int[1000]; private int head = 0; private int tail = 0; private int size = 0; // 入队列 public void put(int value) { if (size == items.length) { return; } items[tail] = value; tail++; if (tail >= items.length) { tail = 0; } size++; } // 出队列 public Integer take() { int result = 0; if (size == 0) { return null; } result = items[head]; head++; if (head >= items.length) { head = 0; } size--; return result; } }
上述代码只是一个简单的环形队列,如果在多线程中进行操作的话,会出现线程安全问题,所以接下来我们要做的是就是解决上述线程安全问题
- 首先呢。我们要保证同一个对象,在出队列时不能入队列,在入队列时不能出队列
所以我们使用 synchronized 进行加锁控制
- 其次。put 插入元素的时候, 判定如果队列满了, 就进行 wait. (注意, 要在循环中进行 wait. 被唤醒时不一
定队列就不满了, 因为同时可能是唤醒了多个线程).take 取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)
所以我们在判断为满或者为空时,使用while循环进行判断
代码实现如下:
public class MyBlockingQueue { private int[] items = new int[1000]; private int head = 0; private int tail = 0; private int size = 0; // 入队列 public void put(int value) throws InterruptedException { synchronized (this) { while (size == items.length) { // 队列满了, 此时要产生阻塞. // return; this.wait(); } items[tail] = value; tail++; if (tail >= items.length) { tail = 0; } size++; // 这个 notify 唤醒 take 中的 wait this.notify(); } } // 出队列 public Integer take() throws InterruptedException { int result = 0; synchronized (this) { while (size == 0) { //return null; // 队列空, 也应该阻塞. this.wait(); } result = items[head]; head++; if (head >= items.length) { head = 0; } size--; // 唤醒 put 中的 wait this.notify(); } return result; } }
测试代码如下:
public class ThreadDemo2 { public static void main(String[] args) { MyBlockingQueue queue = new MyBlockingQueue(); Thread customer = new Thread(() -> { while (true) { try { int result = queue.take(); System.out.println("消费: " + result); } catch (InterruptedException e) { e.printStackTrace(); } } }); customer.start(); Thread producer = new Thread(() -> { int count = 0; while (true) { try { System.out.println("生产: " + count); queue.put(count); count++; Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } }); producer.start(); } }
测试结果为:
⭕总结
关于《【JavaEE初阶】 阻塞式队列详解》就讲解到这儿,感谢大家的支持,欢迎各位留言交流以及批评指正,如果文章对您有帮助或者觉得作者写的还不错可以点一下关注,点赞,收藏支持一下!