目录
阻塞队列
阻塞队列,是一个特殊的队列,虽然继承了队列先进先出的特性,但是也带有特殊的功能:
1)如果队列为空,执行出队列操作,就会阻塞,一直阻塞到另一个线程往队列里添加元素(队列不为空)为止.
2)如果队列满了,执行入队列操作,也会阻塞,一直阻塞到另一个线程取走队列中的元素(队列不满)为止.
基于阻塞队列的阻塞特性,可以实现"生产者消费者模型"
"生产者消费者模型"
那么什么是"生产者消费者模型"呢??
举一个例子来理解:A和B和C包饺子(擀饺子皮+包饺子),有两种包法
1.A和B和C各自进行擀饺子皮+包饺子 操作
2.A负责擀饺子皮,B和C负责包饺子
上述两种方式,显然是第二种效率更高一些,第一种方式大家都会竞争擀面杖从而影响效率.
第二种方式,我们就可以理解为"生产者消费者模型",A负责擀饺子皮,那么A就是生产者,B和C拿走A的饺子皮来包饺子,B和C就是消费者.
那么对应到多线程中,生产数据的线程就是生产者,消费数据的线程就是消费者.
生产者消费者模型的好处
"生产者消费者模型"能给我们的程序带来两个非常重要的好处!!!
实现了发送方和接受方之间的"解耦"
"解耦"就是降低耦合的过程.
我们举一个例子来理解这种模型是怎么解耦的:我们在开发当中,经常会用到服务器之间的相互调用
我们模拟一个对游戏进行充值的场景:
客户端将充值的操作发送给A服务器,此时A服务器把请求发送给B处理,B处理完了就将结果反馈给A,此时就可以视为"A调用了B"
那么,在上述的场景下,我们可以认为A和B之间的耦合是比较高的,因为A要调用B,那么A势必要知道B的存在,如果B崩溃了,就很容易引起A的bug!
另外,如果要添加一个C服务器,此时也需要对A修改不少的代码,因此就需要对A重新修改代码,重新测试,重新发布部署....这样就非常麻烦了.
针对上述场景,我们采用生产者消费者模型,就可以有效的较低耦合!!!
我们采用了生产者消费者模型之后,A和B之间的耦合就降低了很多.
A是不知道B的存在,A只知道队列.(A中的代码中没有任何一行代码和B相关)
B也不知道A的存在,B也只知道队列(B的队列中也没有一行代码和A相关)
如果B崩溃了,对于A没有任何影响,因为队列还好着,A仍然可以给队列插入元素.如果队列满了,就阻塞了.
如果A崩溃了,也对于B没有影响,因为队列还好着,B仍然可以从队列中取元素,如果队列空了,也就先阻塞就好了.
A,B任何一方崩溃了都不会对对方造成影响!!!
新增一个C服务器作为消费者,对于A来说也是没有感知的.
"削峰填谷",保证系统的稳定性
如何理解"削峰填谷"?? 我们可以用三峡大坝来类比着理解.
"削峰":如果上游的水多了,三峡大坝就关闸蓄水.此时就相当于三峡大坝承担了上游的冲击,对下游起到了保护作用.
"填谷":如果上游水少了,三峡大坝就开闸放水.有效保证下游的用水情况.
我们进行服务器开发,也和上述模型十分相似!!!
我们的上游就是用户发送的请求,下游就是一些执行具体业务的服务器.
用户在什么时间发送多少请求,这是不可控的,有时候请求多,有时候请求少.如果在某个时间,很多用户都来发送请求,此时如果没有充分的准备,服务器很容易就挂掉.使用"生产者消费者模型"可以有效防止这种情况的发生,从而保证系统的稳定性.
模型代码
public class ThreadDemo22 {
public static void main(String[] args) {
BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();
//创建两个线程,来作为生产者和消费者
Thread customer = new Thread(()->{
while(true){
try {
Integer result = blockingQueue.take();
System.out.println("消费元素: "+ result);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
customer.start();
Thread producer = new Thread(()->{
int count = 0;
while (true){
try {
blockingQueue.put(count);
System.out.println("生产元素: "+ count);
count++;
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
producer.start();
}
}
使用标准库提供的阻塞队列
标准库提供的阻塞队列:
基于链表的阻塞队列;基于数组实现的阻塞队列;带有优先级的阻塞队列
Queue提供的方法有三个:
- 入队列 offer
- 出队列 poll
- 取队首元素 peek
阻塞队列主要的方法有两个:
- 入队列 put
- 出队列 take
这两个方法是带有阻塞功能的
阻塞队列中也带有offer,poll等方法,但是不具备阻塞功能
使用阻塞队列:
可以看到队列为空时,再取元素,就发生了阻塞等待,线程一直没有结束.
实现一个阻塞队列
实现一个阻塞队列,首先要实现一个普通的队列.
普通的队列可以基于数组,也可以基于链表,这里我们用环形数组来实现.
入队列时,元素放到tail下标的位置,然后tail++.
出队列时,返回head下标的元素,然后head++.
循环队列势必要涉及到区分队列是满是空的问题:head和tail重合,队列可能是空,也可能是满.
解决方法有两种:
- 浪费一个元素,当tail+1==head时,就视为队列满了,tail位置不在放置元素.
- 引入一个size,来记录个数,当size等于数组长度时,就视为队列满了.
先实现一个普通的队列:
当前完成了普通队列的实现,加上阻塞功能,就意味着队列要在多线程环境下使用.
为了保证线程的安全,就要加锁,使用synchronized.
接下来,我们就需要对队列加上阻塞功能.实现阻塞功能,我们使用的时wait和notify关键字来实现.
那么,这两个线程中的wait是否可能同时触发呢?
(如果同时触发,就不能正确的相互唤醒了)
显然时不能同时触发的,针对同一个队列来说,不能够即是满又是空的.
上述代码还有一个瑕疵
当wait被唤醒的时候,此时if的条件一定就不成立了吗??
具体来说,put中的wait被唤醒,要求是队列不满,但是wait被唤醒之后,队列一定是不满的吗?
注意:在我们当前代码中,确实不会出现这种情况,当前我们代码的逻辑一定是取元素成功了才唤醒,而且每次取元素都会唤醒put中的wait.但是稳妥起见,最好的办法,是wait之后再次判定一下,看此时的条件是否具备了,然后继续执行下面的放元素的代码.
完整代码
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
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++;
//对tail进行处理
//1)
//tail = tail % items.length;
//2)
if(tail >= items.length){
tail = 0;
}
size++;
//唤醒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 ThreadDemo23 {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();
//创建两个线程,来作为生产者和消费者
Thread customer = new Thread(()->{
while(true){
try {
Integer result = blockingQueue.take();
System.out.println("消费元素: "+ result);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
customer.start();
Thread producer = new Thread(()->{
int count = 0;
while (true){
try {
blockingQueue.put(count);
System.out.println("生产元素: "+ count);
count++;
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
producer.start();
}
}