目录
阻塞队列
队列是一个先进先出的数据结构,阻塞队列是带有特殊功能的队列
1.如果队列为空,执行出队列操作,就会阻塞,阻塞到另一个线程往队列里添加元素(队列不空)为止。2.如果队列满了,执行入队列操作,也会阻塞,阻塞到另一个线程从队列取走元素位置(队列不满)
消息队列
消息队列也是特殊的队列,相当于是在阻塞队列的基础上,加上了个“消息的类型”按照制定类型进行先进先出。比如学校组织在医院体检时做b超时,我们首先要排队,但是能检查胃,心脏等等,护士叫患者的时候,会说:下一个,来一个检查心脏的,我们进行先进先出的时候,同时也按一定的类型先进先出,把这个消息队列实现为程序,这个程序,可以通过网络的方式和其他程序进行通信,这是这个消息队列,就可以单独部署到一组服务器上(分布式),存储能力和转发能力都大大提升了。因此消息队列已经成为了和mysql相提并论的一个重要组件“中间件”了
生产者消费者模型
就类似于包饺子:1.每个人分别进行擀饺子皮+包饺子操作2.一个人专门负责擀饺子皮,另外三个人负责包,我每次擀好一个饺子皮,就放到盖帘上,他们每次都从盖帘上取一个皮进行包,第一种,大家会竞争擀面杖,就会产生阻塞等待,影响效率,第二种就是我负责擀饺子皮,我就是生产者,他们三个负责包,就是消费者,盖帘就是阻塞队列,如果我擀的太慢了,他们就得等,如果我擀的太快了,我就得等一会。擀饺子皮的人不关心包饺子的人是谁,不管是机器包还是手工包,包饺子的人也不管擀饺子皮的人是谁。
好处:1.实现了发送方和接收方之间的“解耦” 降低耦合的过程,就叫做“解耦”,代码的要求就是要追求“低耦合”,开发中典型的场景:服务器之间的相互调用
此时A把请求给了B,B处理完有返回给A,此时A和B的耦合时比较高的,A调用B,A无比要知道B 的存在,如果B出问题了,很容易引起A的bug,如果再加一个C服务器,此时要对A修改不少的代码,非常麻烦,此时,如果使用生产者消费者模型,就可以有效的降低耦合
此时A 和B的耦合已经降低很多了,A不知道B的存在,B也不知道A的存在,A中没有一行代码时和B相关的,如果B出问题,对A没有任何影响,,因为队列还好着,A依然可以给队列插入元素,如果队列满了,就阻塞就可以了,如果新增一个C来作为消费者,对A来说也时没有任何影响的
2.可以“削(xue)峰填谷”,保证系统的稳定性
三峡大坝就好比阻塞队列,削峰填谷,如果上游水多了,就关闭蓄水,水少了就开闸放水,对下游起到了很好的保护作用。对上游什么时候涨水什么时候水少是不知道的,上游就是用户发送的请求,下游就是执行任务的服务器,用户发送多少请求,我们是不知道的,所以就要使用生产者消费者模型,来做好准备。
生产者消费者代码实现
import java.beans.IntrospectionException;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.LinkedBlockingQueue;
public class ThreadDemo {
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) {
e.printStackTrace();
}
}
});
customer.start();
Thread prouducer = new Thread(()->{
int count = 0;
while(true){
try{
blockingQueue.put(count);
System.out.println("生产元素"+count);
count++;
Thread.sleep(500);//每隔500秒生产一个
}catch (InterruptedException e){
e.printStackTrace();
}
}
});
prouducer.start();
}
}
每生产一个就会消费一个,但是生产线程和消费线程是并发执行的,所以谁先启动是都有可能的,整体来说是成对出现的
实现一个阻塞队列
要实现一个阻塞队列,需要先写一个普通的队列,可以基于链表,也可以基于数组,基于数组恒容易进行头删尾插,一个链表的头删操作,时间复杂度是o(1),尾插操作也可以是o(1),只要用一个额外的引用,记录当前的尾节点即可,头插尾删也是可以的。同样也可以基于环形数组,默认情况,这个数组每个元素虽然已经开辟了内存空间,但是视为上面的元素是无效元素,构成一个前闭后开[head,tail),添加一个元素,tail++;head和tail重合可能是空也可能是满,我们可以浪费一个元素,或者引入一个size,来记录个数。
阻塞功能意味着,在单线程下,如果队列为空,执行出队列操作,那么就会阻塞,既然自己阻塞了,就没办法去添加元素,所以队列要在多线程坏境下使用,那么我们首先要保证线程安全。如果对两个线程同时修改一个变量就会出现线程安全问题,所以我们要对线程加上锁synchronized(this)保证线程安全,使用this表示针对同一个队列去put和take的时候会出现锁竞争,针对不同队列没有锁竞争,两个wait不会同时触发,if条件不同,但是wait被唤醒的时候,此时的if的条件,此时if的条件,一定就不成立了吗?具体来说,put中的wait被唤醒,要求队列不满,但是外套被唤醒了之后,队列一定是不满的吗?当前的代码确实不会出现这种情况,当前代码一定是去元素成功才唤醒,每次去元素都会唤醒,但稳妥起见,最好的办法是wait返回之后再判定一下,看此时的条件是不是具备了,所以将if换为while。就类似于早上起床,要定个闹钟,定个九点的闹钟,正常情况下,闹钟响了,就该起床了,但是如果七点就醒了,就发现,醒早了(条件还不满足),那我就可以再睡会,每次醒了都看下时间,判断一下当前是否满足起床的条件。
//自己实现一个阻塞队列
class MyBlockingQueue{
//基于数组实现
//变量
private int[] items = new int[1000];
private int head = 0;
private int tail = 0;
private int size = 0;
//入队列和出队列的wait不会同时触发 针对同一个队列 不能又是满的又是空的
//入队列
public void put(int value) throws InterruptedException {
synchronized (this){
//if(size == items.length)
while(size == items.length) {
//return
//队列满了 此时要产生阻塞,不能继续插入,
// 阻塞到另一个元素出去,所以就要加入wait notify
this.wait();
}
items[tail] = value;
tail++;
//对tail的处理
//1)
//tail = tail % items.length;
//2)可读性更好 这个代码的效率可能比%更高
if (tail >= items.length) {
tail = 0;
}
size++;
//这个notify唤醒take 中的wait
this.notify();
}
}
public Integer take()throws InterruptedException {
int result = 0;
synchronized (this) {
//if(size == 0)
while (size == 0) {
//队列为空,不能出队列
// return null;
this.wait();
}
result = items[head];
head++;
if (head >= items.length) {
head = 0;
}
size--;
//唤醒put中的wait
this.notify();
}
return result;
}
}