目录
之前在数据结构中学的队列是最基础的队列,在实际开发中针对队列还有很多形式:(1)普通队列 ,线程不安全 (2)优先级队列:以堆为基础的数据结构,线程不安全 (3)阻塞队列:线程安全,先进先出,带有阻塞功能。++当队列为空时尝试出队操作就会阻塞等待,一直阻塞到队列不为空为止。同理,当队列为满时尝试入队列也会阻塞等待,阻塞到队列不为满为止。++BlockingQueue就是标准库提供的阻塞队列。(4)消息队列------生产者消费者模型
生产者消费者模型
由于消息队列这样的数据结构特别好用,实际开发中经常把这样的数据结构封装成单独的服务器程序中单独部署,这样的服务器程序同样也称为++消息队列++。
消息队列能起到的作用------生产者消费者模型。
生产者消费者模型在开发中主要有两方面的意义:
(1)能让程序解耦合 (2)能够使程序"削峰填谷"
普通的队列也可以实现这个模型,具体要看实现的场景*,如果需要在一个进程内实现生产者消费者模型的话直接使用阻塞队列即可,如果需要在分布式系统实现生产者消费者模型那就需要单独部署的消息队列服务器。*
生产者消费者模型是一种典型的解决多线程问题的方式。比如说:当ABC三人分工合作包饺子,包饺子需要先擀皮再包,但擀面杖只有一个,ABC三人难免会去竞争擀面杖,此时用专业术语来讲就是"锁竞争",如果分配A来擀皮,擀完的皮放到面板上让B和C来包,这个时候A就是生产者,B和C就是消费者,面板就相当于是阻塞队列/消息队列;如果A擀的块,B和C包得慢,当面板放满的时候A就陷入阻塞等待,B和C在面板上消费后A才能继续生产,同理B和C包的比A擀得快的话,BC就会阻塞等待A生产,A->面板->BC此时就是生产者消费者模型。
生产者消费者模型优势
生产者消费者模型有很多优势,最主要的两个:
(1)解耦合
当有线程A和线程B,让A直接调用B意味着A代码中要包含很多B的逻辑,B代码里也要包含A相关的逻辑,彼此间会有一定的耦合。++一旦对A进行修改就可能会对B造成影响++ ,一旦A出bug也可能会牵连到B,这种情况下的模型可以简单画为:
但如果采用生产者消费者模型,在AB中间加一块"面板",此时可以画为:
此时站++在A的视角,只需要关心和队列的交互,站在B的视角也只需要关心和队列的交互++ 。如果对A的代码进行修改就不太能影响到B,++大大降低了AB线程之间的耦合++ ,未来如果引入别的线程也不需要对A进行修改,直接让新线程从队列中读取数据就可以。++提高了代码的可扩展能力++。
(2)削峰填谷
客户端发来的请求数量多少没法提前预知,遇到突发事件可能会导致客户端发来的请求激增。A来接受客户端发来的大量请求,然后将请求发到消息队列,B从消息队列获取任务去执行,无论A给队列写多快,B都能按照自己的节奏消费数据,相当于队列将B保护了起来。
正常情况下,A收到一个客户端请求也会请求一次B,A收到的请求激增了B的请求也会激增,A的工作比较简单消耗资源少,B的工作更复杂消耗的资源更多,一旦请求多了就容易挂。
(挂:服务器每次处理一个请求都要消耗一定的系统资源,如果同一时刻要处理的请求太多,消耗的总资源数目超出机器能提供的上线,机器就挂了)
三峡大坝起到的效果就是削峰填谷。
通过代码看一下生产者消费者模型(使用阻塞队列)
使用标准库提供的BlockingQueue
ArrayBlockingQueue->使用数组
LinkedBlockingQueue->链表
PriorityBlockingQueue->堆
主要使用put&take,其他的offer&add&poll是继承原队列的方法,不带有阻塞功能,看BlockingQueue的源码可以看到++继承了Queue接口。++
自己实现阻塞队列
阻塞队列是线程安全带有阻塞功能的普通队列,所以自行实现类似于阻塞队列的方法时可以用以下几步:++(1)写一个普通队列 (2)加上线程安全 (3)加上阻塞功能++
++第一步:使用数组实现普通队列,用两个变量记录队首队尾的下标,还需引入一个变量记录当前数组的元素个数,记录满/空++。第一步的代码写完后为:
java
class MyBlockingQueue{
private int[] array = null;
private int head;
private int tail;
private int size;
void put(int num){
if(array.length >= size ){
return; //当队列为满时入队操作应该阻塞等待,后续步骤再进行修改
}
array[tail] = num;
tail++;
if(tail >= array.length){
tail = 0;
}
size++;
}
int take(){
if(size == 0){
return 0; //当队列为空时出队列应该阻塞等待,我们先完成第一步,后续再进行修改
}
int ret = array[head];
head++;
if(head >= array.length){
head = 0;
}
size--;
return ret;
}
}
第二步:将线程安全和阻塞的逻辑加上,代码为:
java
class MyBlockingQueue{
private int[] array = null;
private volatile int head;
private volatile int tail;
private volatile int size;
void put(int num) throws InterruptedException {
synchronized (this){ //(2)给关键操作加锁,加上线程安全
if(array.length >= size ){
this.wait(); //(1)当队列满时,当前线程进入等待,当队列被take时解除等待
}
}
array[tail] = num;
tail++;
if(tail >= array.length){
tail = 0;
}
size++;
this.notify();
}
int take() throws InterruptedException {
synchronized (this){
if(size == 0){
this.wait();
}
}
int ret = array[head];
head++;
if(head >= array.length){
head = 0;
}
size--;
this.notify();//(3)将阻塞中的线程唤醒
return ret;
}
}
逻辑步骤已经在代码中进行表示
需要注意:++(1)唤醒时使用notifyall是不合理的,比如说有两个线程put时被阻塞了,一次take将所有等待线程唤醒了,一个线程可以put成功,而另一个put就要出bug了++
++(2)当鼠标放在wait上时可以看见系统给出的信息++
大概意思是wait除了会被notify¬ifyall唤醒,还会被别的interrupt唤醒,搭配while使用可以在被唤醒之后再次进行判定是否符合唤醒条件,是否该继续进行。将if改为while就是我们自己实现的MyBlockingQueue的最后一步优化。
最后优化完的代码为:
java
class MyBlockingQueue{
private int[] array = null;
private volatile int head;
private volatile int tail;
private volatile int size;
void put(int num) {
synchronized (this){
while(array.length >= size ){
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
array[tail] = num;
tail++;
if(tail >= array.length){
tail = 0;
}
size++;
this.notify();
}
int take() {
synchronized (this){
while(size == 0){
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
int ret = array[head];
head++;
if(head >= array.length){
head = 0;
}
size--;
this.notify();
return ret;
}
}
本篇文章到这里就结束了,感谢观看。
下篇文章更新线程池相关内容
感谢观看
道阻且长,行则将至