【JavaEE初阶】深入理解多线程阻塞队列的原理,如何实现生产者-消费者模型,以及服务器崩掉原因!!!

服务器 0

前言:

🌈上期博客:【JavaEE初阶】深入解析单例模式中的饿汉模式,懒汉模式的实现以及线程安全问题-CSDN博客

🔥感兴趣的小伙伴看一看小编主页:GGBondlctrl-CSDN博客

⭐️小编会在后端开发的学习中不断更新~~~  

🥳非常感谢你的支持

目录

📚️1.阻塞队列

 1.1阻塞队列的特性

1.2阻塞队列的使用

1.实例化

 2.入队列

3.出队列

1.3阻塞队列的实现

1.构造环形队列,实现出入队列功能

2.在多线程中解决线程安全问题

3.多线程实现阻塞唤醒操作

 📚️2.生产者-消费者模型

2.1生产者消费者模型的模拟 

2.2生产者消费者模型的作用

1.解耦合

2.削峰填谷

2.3生产者消费者模型代价

2.4生产者消费者模型模拟

📚️3总结


📚️1.阻塞队列

 1.1阻塞队列的特性

所谓的阻塞队列即对于普通队列做出的扩展,这个队列具有普通队列的特性,先进先出,并且阻塞队列是线程安全的,具有阻塞的功能~~~

阻塞功能:

如果针对一个已经满了的阻塞队列,在往队列里加入数据,此时队列就会发生阻塞,直到阻塞到队列不为满为止~~~

如果针对一个已经空了的阻塞队列,在往队列里取出数据,此时队列就会发生阻塞,直到阻塞到队列不为空为止~~~

1.2阻塞队列的使用

1.实例化

在Java标准库中有阻塞队列的数据结构,如下代码所示:

 BlockingQueue<Integer> queue=new ArrayBlockingQueue<>(4);

 这里的ArrayBlockingQueue<>即用数组实现的阻塞队列,里面的4就是存储量

这里还存在用链表实现,和优先级队列实现的优先级队列,如下代码所示:

 BlockingQueue<Integer> queue=new ArrayBlockingQueue<>(4); BlockingQueue<Integer> queue1=new LinkedBlockingQueue<>(4); BlockingQueue<Integer> queue2=new PriorityBlockingQueue<>(4);
 2.入队列

这里使用put或者offer来进行入队列,但是这里的put具有阻塞的功能,而offer没有,代码如下:

queue.put(1);queue.put(2);queue.put(3);queue.put(4);

这里再添加数据,我们看debug一下,测试:

此时我们能够看到使用put在达到存储最大值时,就不再添加数据了;

使用offer添加数据时,我们再次Debug一下后:

queue.offer(1);queue.offer(2);queue.offer(3);queue.offer(4);System.out.println(queue.offer(5));

此时我们打印一个false,且结束进程,所以offer是不会发生阻塞的,当队列满了的时候,插入数据成功会返回TRUE反之返回一个FALSE;

3.出队列

 这里就是使用take来进行出队列,且这里的take也具有阻塞的功能,代码如下:

queue.offer(1);queue.offer(2);queue.offer(3);queue.offer(4);System.out.println(queue.take());System.out.println(queue.take());

当阻塞队列为空的时候,出队列会发生阻塞,即进程无法结束~~~ 

1.3阻塞队列的实现

 这里小编将使用数组来进行阻塞队列的实现,这里的队列就是环形队列;

1.构造环形队列,实现出入队列功能

代码如下:

class Myblockingqueue{    //声明一个环形数组    private int[] elems=null;    private int head=0;    private int tail=0;    private int size=0;       //在实例化时,初始化这个数组    public Myblockingqueue(int capcity){        elems=new int[capcity];    }    public void put(int elem)  {                                     if (size==elems.length){                return ;            }            elems[tail]=elem;            size++;            tail++;            if (tail>elems.length-1){                tail=0;            }                        }    public int take() {            int elem=0;                         if (size==0){                               return 0;            }            elem=elems[head];            head++;            size--;            if (head>elems.length-1){                head=0;            }                       return elem;            }}

这里对于环形队列的实现,小编就不再过多赘述了,前面数据结构有讲解

2.在多线程中解决线程安全问题

当多线程情况下,就会有以下问题,如图所示:

注意:这里当put最后一个数据的时候 线程1判断完条件后,别调度走了,然后此时线程2也满足条件,此时tail为100(假设存贮为100),然后线程1又调度回CPU执行剩下的,再次添加,那么就存在101个数据了,此时就发生了线程安全问题

解决:此时就需要加锁,使这串代码性质改变为原子性,使cpu调度其他线程发生阻塞

代码如下:

 synchronized (lock){            //判断条件,是否满了,满了就进入阻塞            if(size==elems.length){                return;            }            elems[tail]=elem;            size++;            tail++;            if (tail>elems.length-1){                tail=0;            }                 }

此时的出队列方法也是一样的,进行加锁,解决线程安全问题;

3.多线程实现阻塞唤醒操作

 在上述理解中,我们当队列为满的时候,入队列方法就要进行阻塞,那么唤醒时机就是出队列后;队列为空的时候,出队列就要进入阻塞,那么唤醒时机就是入队列之后;

入队列方法如下:

 synchronized (lock){            //判断条件,是否满了,满了就进入阻塞            if(size==elems.length){                lock.wait();            }            elems[tail]=elem;            size++;            tail++;            if (tail>elems.length-1){                tail=0;            }            //唤醒取出线程            lock.notify();        }

 同理出队列方法如下:

        int elem=0;        //判断条件,如果为空,那么就进入阻塞        synchronized (lock){            if (size==0){                //进入线程等待                lock.wait();            }            elem=elems[head];            head++;            size--;            if (head>elems.length-1){                head=0;            }            lock.notify();            return elem;        }

此时就解决了阻塞和唤醒线程的操作,但是此时任然有问题~~~

注意:当有两入队列线程,此时出队列后唤醒一个入队列线程后,思路是添加数据后满了需要唤醒出队列操作,但是由于锁对象是一样的,可能唤醒的就是另一个线程的入队列操作~~~

图示:

 

由于if只是判断一次,由于多线程的原因,肯会有很多变数,所以在被唤醒后,还需要判断是否满足队列满的条件,那么解决办法即将if改为while

代码如下:

public void put(int elem) throws InterruptedException {        //防止多线程状态下,发生线程安全问题        synchronized (lock){            //判断条件,是否满了,满了就进入阻塞            while (size==elems.length){                lock.wait();            }            elems[tail]=elem;            size++;            tail++;            if (tail>elems.length-1){                tail=0;            }            //唤醒取出线程            lock.notify();        }    }

 这里就是入队列的操作,那么同理出队列也是一样的,需要就那些while的循环判断~~~,那么此时我们的阻塞队列的实现就完成了;

 📚️2.生产者-消费者模型

注意:生产者消费者模型是根据阻塞队列来进行实现的 

2.1生产者消费者模型的模拟 

这里小编可以使用一个包饺子的过程来为大家展示这个模型的现实场景

在包饺子中,我们一般是一个人擀面,然后另外两个(假设)人进行包饺子,图示如下:

此时这里两个包饺子的人就是消费者线程,而擀面的人就是一个生产者线程;

此时的桌子即是一个阻塞队列,负责接收面皮,并传递面皮给包饺子的人;

注意:

假设擀面的人,擀饺子皮很快,那么此时就是生产大于消费,那么很快桌子就会被饺子填满,此时擀面的人就不能擀面了(类似于阻塞),然后两个包饺子的人包一个,擀面的就生产一个 面皮;

假设包饺子的人,包得很快,那么此时的消费就大于生产,那么很快桌子就为空了,那么包饺子的人就得等擀面的人(类似于阻塞),然后就是擀面的人擀出来一张面皮,包饺子的人就消费一张;

2.2生产者消费者模型的作用

1.解耦合

解耦合:将代码的耦合程度又高降低叫做解耦合~~~

在实际开发中,通常是“分布式系统”,即整个功能不是由一台服务器来完成的,而是每个服务器负责一部分功能,然后通过网络通信来进行连接,共同完成的;

那么当我们不使用阻塞队列时:

问题:此时由于没阻塞队列,A和B的耦合性就很强,A和C的耦合性就很强,A中的代码设计到B的一些相关操作,B也设计到A的一些相关操作,此时如要更改B,那么A也会受到影响,同理A和C也是一样的;

 那么加入阻塞队列:

注意:此时阻塞队列就是一个中介,服务器A不知道服务器B和C,此时A只需要传递数据给阻塞队列,再由阻塞队列传递给B和C,此时相应的耦合性就打打降低了;之间的影响也下降了;

2.削峰填谷

由于请求是客户端一方的需求,所以存在一时间突然请求骤增,那么此时就存在服务器崩掉的问题

就这个图来说:由于服务器A的功能简单,就传递数据给对应的功能服务器,那么它的抗压能力就更强,而BC服务器的功能复杂,需要消耗更多的资源,所以抗压能力更差; 

服务器挂掉原因:

由于服务器要处理每个请求,所以需要消耗硬件资源,这里包括(CPU,硬盘,内存,和网络带宽),此时其中一个硬件资源达到瓶颈,那么此时就不会对请求做出响应了,就导致服务器挂掉了

当我们引入阻塞队列时:

此时服务器A的数据传递给阻塞队列,由于阻塞队列只是用于存贮数据,所以抗压能力更强,可以控制请求传给功能服务器的速度,防止服务器B和C被大量的请求冲击而垮掉~~~

2.3生产者消费者模型代价

1.上述描述的阻塞队列,是以这个数据结构来实现的的服务器结构,会部署到单独的主机上

2.整个系统的结构变得复杂,需要维护的服务器就更多,成本变高了;

3.通过阻塞队列,实现A到B的数据传输的转化,需要一定的开销,影响效率

2.4生产者消费者模型模拟

此时我们就要用一个线程调用阻塞队列的入队列模仿生产者,一个线程调用出队列模仿消费者

代码如下:

 public static void main(String[] args) {        Myblockingqueue queue=new Myblockingqueue(100);              //进行两个线程的测试        Thread t1=new Thread(()->{            int n=1;            while (true){                try {                    queue.put(n);                    System.out.println("生产的元素"+n);                    n++;//当太多了就进入阻塞                } catch (InterruptedException e) {                    throw new RuntimeException(e);                }            }        });        Thread t2=new Thread(()->{            while (true){                try {                    Thread.sleep(500);                    int n=queue.take();                    System.out.println("消费的元素"+n);                } catch (InterruptedException e) {                    throw new RuntimeException(e);                }            }        });        t1.start();        t2.start();    }

解释:此时代码由于消费者的消费进行了休眠操作,那么输出就是现生产大量的元素,然后消费一个生产一个;

如图:

这里小编只显示了一部分输出;

如果将休眠放在生产部分,那么代码如下:

 Thread t1=new Thread(()->{            int n=1;            while (true){                try {                    queue.put(n);                    System.out.println("生产的元素"+n);                    n++;//当太多了就进入阻塞                    Thread.sleep(500);                } catch (InterruptedException e) {                    throw new RuntimeException(e);                }            }        });

那么输出就是:

消费一个,生产一个~~~

📚️3总结

💬💬小编本期讲解了关于阻塞队列的特性,实现过程中存在的问题,以及解决和代码的实现,并且还利用了阻塞队列实现了生产者消费者模型;并且还理解了生产者消费者模型在实际开发中作用

🌅🌅🌅~~~~最后希望与诸君共勉,共同进步!!! 


💪💪💪以上就是本期内容了, 感兴趣的话,就关注小编吧。

                             😊😊  期待你的关注~~~

也许您对下面的内容还感兴趣: