1.0 阻塞队列
普通队列,优先级队列,阻塞队列(带有阻塞功能)
阻塞队列概念:
- 阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则. 阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:
- 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
- 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.
作用:
- 阻塞队列的一个典型应用场景就是 “生产者消费者模型”. 这是一种非常典型的开发模型.
生产消费者模型
理解生产消费者模型:
- 生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
- 生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.
- **例子:**比如过年一家人一起包饺子. 一般都是有明确分工, 比如一个人负责擀饺子皮, 其他人负责包. 擀饺 子皮的人就是 “生产者”, 包饺子的人就是 “消费者”. 擀饺子皮的人不关心包饺子的人是谁(能包就行, 无论是手工包, 借助工具, 还是机器包), 包饺子的人 也不关心擀饺子皮的人是谁(有饺子皮就行, 无论是用擀面杖擀的, 还是拿罐头瓶擀, 还是直接从超 市买的).而生产者和消费者交易东西的地方就是交易场所(也叫阻塞队列),可以降低生产者和消费者之间的关联,不会应为一个崩溃其他都崩溃生产者和消费者都崩溃
生产消费者模型 和 阻塞队列的 优势:
- **阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力. **比如在 “秒杀” 场景下, 服务器同一时刻可能会收到大量的支付请求. 如果直接处理这些支付请求, 服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程). 这个时候就可以把这些请求都放 到一个阻塞队列中, 然后再由消费者线程慢慢的来处理每个支付请求. 这样做可以有效进行 “削峰”, 防止服务器被突然到来的一波请求直接冲垮.
- **阻塞队列也能使生产者和消费者之间解耦合.**比如A服务器(生产者)要向B服务器(消费者)发数据,则创建一个阻塞队列(作为交易场所),这样A,B互不关心对方,只从阻塞队列中发和去数据,这样A,B的耦合(关联性)就下降了,并且A,B某个服务器崩溃也不会对对方造成影响,并且添加新的服务器要取数据也不用给修改A的代码和B的代码,直接从阻塞队列中取
- 削峰填谷,峰值往往是短暂的,为了不让生产者和消费者服务器负担差距太大,利用阻塞队列让生产者(处理数据快)放数据到阻塞队列(削峰),等峰值过了,消费者(处理数据慢)也能不闲着,一直能取出数据(填谷)
生产消费者模型 和 阻塞队列的 缺点:
- 会降低效率(影响小)
生产消费者的其他应用:
正因为生产消费者模型这么重要,虽然阻塞队列只是一个数据结构,我们会把这个数据结构是现成一个服务器程序,并且使用单独的主机/主机集群 来部署,此时这个所谓的阻塞队列就进化成了"消息队列"
标准库中的阻塞队列
在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可.
- BlockingQueue 是一个接口(不能直接new,要用专门的方法). 真正实现的类是 LinkedBlockingQueue.
- put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
- BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.
语法
//创建
BlockingQueue<String> queue = new LinkedBlockingQueue<>(); //基于链表,括号内可以指定阻塞队列最大容量
BlockingQueue<String> queue = new PriorityBlockingQueue<>();// 基于堆实现的
BlockingQueue<String> queue = new ArrayBlockingQueue<>(); //基于数组实现的的
// 入队列
//要抛异常应为可能会队列满而阻塞
queue.put("abc");
// 出队列. 如果没有 put 直接 take, 就会阻塞(即队列为空时take就会阻塞).
String elem = queue.take();
//对于BlockingQueue 只有put和take才有阻塞功能
代码例子:
//生产消费者模型
//可以用阻塞队列削峰填谷
public class Demo19 {
public static void main(String[] args) {
//搞一个阻塞队列,作为交易场所
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
//负责生产元素
Thread t1 = new Thread(()->{
int count=0;
while(true){
try {
queue.put(count);
System.out.println("生产者元素添加 ="+count);//答应在添加后
count++;
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//负责消费
Thread t2 = new Thread(()->{
while(true){
try {
//这里不需要sleep也可以让生产那跟着消费走,且互不影响
Integer n = queue.take();
System.out.println("消费元素 ="+ n);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}
2.0 模拟实现阻塞队列
-
通过 “循环队列” 的方式来实现.
-
使用 synchronized 进行加锁控制.
-
put 插入元素的时候, 判定如果队列满了, 就进行 wait. (注意, 要在循环中进行 wait. 被唤醒时不一
-
定队列就不满了, 因为同时可能是唤醒了多个线程).
-
take 取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)
-
阻塞可以被notify 和interrupt 唤醒,notify不会影响后续执行,interrupt会抛异常让程序员自行判断后续的执行
代码实现
package thread;
class MyBlockingQueue {
// 使用一个 String 类型的数组来保存元素. 假设这里只存 String.
private String[] items = new String[1000];
// 指向队列的头部
//volatile 保证每次取和修改时拿到的都是最新的这个数据
volatile int head = 0;
// 指向队列的尾部的下一个元素. 总的来说, 队列中有效元素的范围 [head, tail)
// 当 head 和 tail 相等(重合), 相当于空的队列.
volatile private int tail = 0;
// 使用 size 来表示元素个数.
volatile private int size = 0;
private Object locker = new Object();
// 入队列
public void put(String elem) throws InterruptedException {
// 此处的写法就相当于直接把 synchronized 写到方法上了.
synchronized (locker) {
while (size >= items.length) { //循环判断防止interrupt等打断后未作修改的情况
// 队列满了.
// return;
locker.wait(); //用locker相比于this能更有效地阻止interrupt打断this的情况
}
items[tail] = elem;
tail++;
if (tail >= items.length) {
tail = 0;
}
size++;
// 用来唤醒队列为空的阻塞情况
locker.notify();
}
}
// 出队列
public String take() throws InterruptedException {
synchronized (locker) {
while (size == 0) {
// 队列为空, 暂时不能出队列.
// return null;
locker.wait();
}
String elem = items[head];
head++;
if (head >= items.length) {
head = 0;
}
size--;
// 使用这个 notify 来唤醒队列满的阻塞情况
locker.notify();
return elem;
}
}
}
public class Demo20 {
public static void main(String[] args) throws InterruptedException {
// MyBlockingQueue queue = new MyBlockingQueue();
// queue.put("aaa");
// queue.put("bbb");
// queue.put("ccc");
//
// String elem = queue.take();
// System.out.println("elem=" + elem);
// elem = queue.take();;
// System.out.println("elem=" + elem);
// elem = queue.take();;
// System.out.println("elem=" + elem);
// elem = queue.take();;
// System.out.println("elem=" + elem);
// 创建两个线程, 表示生产者和消费者
MyBlockingQueue queue = new MyBlockingQueue();
Thread t1 = new Thread(() -> {
int count = 0;
while (true) {
try {
queue.put(count + "");
System.out.println("生产元素: " + count);
count++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
while (true) {
try {
String count = queue.take();
System.out.println("消费元素: " + count);
Thread.sleep(1000); //调整生产和消费者的时间差异,体现阻塞队列的作用
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}