1. 什么是阻塞式队列
阻塞队列是一种特殊的队列. 也遵守 "先进先出" 的原则. 阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:
当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.
阻塞队列的一个典型应用场景就是 "生产者消费者模型". 这是一种非常典型的开发模型.
那什么是生产者消费者模型呢?
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。 生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等 待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.
主要问题有两个:
其一,用户端的“上游请求”的数量不是可控的,如果服务器A在同一时刻收到大量的支付请求,并且将请求直接交给B处理。这样服务器崩溃的概率是十分大的。
其二,两个服务器之间的耦合度太高了。A要调用B的话,A务必是知道B的存在的,如果B挂掉了很容易引起A的bug。此外现在需要再加一个服务器C也来处理工作,那么服务器A上的代码就需要重新部署。
引入阻塞式队列解决问题:
此时A,B不再有直接的任务接触,A讲请求放在阻塞式队列中,并不知道B的存在。B直接从阻塞式队列中拿“请求”,然后进行处理,B也不知道A的存在。这样一来就是一个简单的生产者消费者模型。
这样上面的两个问题就迎刃而解。如果现在“上游”用户的请求激增,阻塞式队列可以在AB之间提供一个缓冲区,B来不及处理的请求可以暂时在阻塞式队列中“存放”,从而达到了“削峰填谷”的目的。如果现在想要增加A或者B类型的服务器c,也不用修改原本的代码重新部署之类的操作,因为两者已经解耦。如果 B 挂了,对于 A 没有任何影响.因为队列还好着,A 仍然可以给队列插入元素.如果队列满,就先阻塞就好了如果 A 挂了,也对于 B 没啥影响.因为队列还好着,B 仍然可以从队列取元素.如果队列空,也就先阻塞就好了A B 任何一方挂了不会对对方造成影响!!!
新增一个 C 来作为消费者,对于 A 来说仍然是无感知的.
总结 生产者消费者模型的两个优点:
1)阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力.
2)阻塞队列也能使生产者和消费者之间 解耦.
2. 标准库中的阻塞式队列
在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可.
BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性
LinkedBlockingDeque<Object> blockedQueue = new LinkedBlockingDeque<>();
//入队列, 如果此时队列满 就回阻塞
blockedQueue.put("zhangsan");
// 出队列. 如果没有 put 直接 take, 就会阻塞.
System.out.println(blockedQueue.take());
生产者消费者模型
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<Integer>();
Thread customer = new Thread(() -> {
while (true) {
try {
int value = blockingQueue.take();
System.out.println("消费元素: " + value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "消费者");
customer.start();
Thread producer = new Thread(() -> {
Random random = new Random();
while (true) {
try {
int num = random.nextInt(1000);
System.out.println("生产元素: " + num);
blockingQueue.put(num);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "生产者");
producer.start();
customer.join();
producer.join();
}
}
3. 写代码实现阻塞式队列
public class MyBlockingQueue {
//通过循环队列的方式来实现
private int[] elems = new int[1024];
private volatile int size = 0;
private int head = 0;
private int tail = 0;
public void put(int value) throws InterruptedException {
synchronized (this) {
//先判断put操作的时候 队列是否已经满了
//注意使用while 而不是if 当线程再次被唤醒的时候,队列不一定就是没满
while (size == elems.length){
wait();
}
elems[tail] = value;
tail = (tail + 1) % elems.length;
size++;
//成功插入一个新的元素后 就通知阻塞的take可以继续take了
notifyAll();
}
}
public int take() throws InterruptedException {
int elem = 0;
synchronized (this) {
while(size == 0){
wait();
}
elem = elems[head];
head = (head + 1) % elems.length;
size--;
//成功拿走一个元素之后,就可以通知阻塞的put可以继续put了
notifyAll();
}
return elem;
}
public synchronized int size() {
return size;
}
// 测试代码
public static void main(String[] args) throws InterruptedException {
MyBlockingQueue blockingQueue = new MyBlockingQueue();
Thread customer = new Thread(() -> {
while (true) {
try {
int value = blockingQueue.take();
System.out.println("消费:"+ value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "消费者");
customer.start();
Thread producer = new Thread(() -> {
Random random = new Random();
while (true) {
try {
int nextInt = random.nextInt(10000);
System.out.println("生产:" + nextInt);
blockingQueue.put(nextInt);
//让生产者慢一点 观察消费者
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "生产者");
producer.start();
customer.join();
producer.join();
}
}