阻塞队列---多线程

常见的队列:

1)优先级队列:底层的数据结构是堆(完全二叉树),出的数据要有一个优先级

2)消息队列:队列中的元素是带有一定的类型,分类信息,出队列的时候,不是单纯的先进先出,而是以分类作为维度,来进行确定某个类先来的元素先出,按照指定的类型来决定先进先出,不同的形状就代表不同类型的病人,有的是检查胃的(圆圈),有的是检查脑子的(五角星),有的是检查(心脏的),他们在排队,门诊室的医生说,检查胃的过来,此时得胃病的就会绕过长方形,进入到门诊室里面,我们在取队列元素的时候,按照类型来取,是取长方形类型的数据,还是五角星类型的数据,确定类型之后,在确定谁排在前面,取谁

1)阻塞队列:是一个线程安全的队列,是可以保证线程安全的

1.1)如果当前队列为空,尝试出队列,进入阻塞状态,一直阻塞到队列里面的元素不为空

1.2)如果当前队列满了,尝试入队列,也会产生阻塞,一直阻塞到队列中的元素不为满为止

1.3)所以在Java的标准库中内置了一个BlockingQueue(是一个接口)这样的类来实现阻塞队列这样的功能,它的用法与普通的入队列和出队列很相似,没有取队首元素的操作;

1.4)Java.util.concurrent这个包里面包含了很多与多线程并发相关的组件操作,简称JUC

  BlockingQueue blockingQueue=new LinkedBlockingQueue<>();//基于链表来实现,可以指定阻塞队列的大小
  blockingQueue.put("hello");
  String str=blockingQueue.take();

1)要处理InterruptedException,通过Interrupted方法来中断这个线程所产生的异常,这个异常,因为它是一个阻塞队列,因为put操作会随时阻塞,而我们阻塞就需要进行唤醒,唤醒有可能就会被打断,所以就会抛出InterruptedException异常,它的底层继承了queue这个类,put带有阻塞功能,从里面取元素一般用take,这两个方法自带有阻塞功能,take从阻塞队列的队首元素取出一个元素的操作
2)里面放一个元素,咔咔取两次,就会出现阻塞的情况,代码不会继续运行,代码会阻塞到take这里,会一直阻塞到下一次塞元素为止

3)作为一个队列,还是提供了一些基本的操作,比如说offer叫做入队列,poll()叫做出队列,peek()获取队首元素,这些方法对于我们的阻塞队列来说也是支持的,但是我们平时还是会经常使用put和take,因为我们的put和take是自带阻塞效果的,而我们的普通队列是没有阻塞效果的,所以我们要使用带有阻塞效果的方法;

会涉及到加锁,对象等待集等问题

为什么要用对象等待集?

1)如果入队列的操作太快,队列满了,继续入队列,就会进入阻塞,一直阻塞到有其他线程去消费队列了,才能出队列,才可以进行入队列操作。

2)如果出队列的操作太快,队列空了,继续出队列,也会阻塞,一直等到被其他线程生产了元素,才会继续出队列;

先明确一个事:wait和notify操作,都是针对count来进行计算

阻塞队列的知识点补充:

1)add方法和offer方法可以将我们指定的元素放到BlockingQueue里面,如果此时阻塞队列可以容纳,那么直接返回true,否则直接返回false;

2)但是put方法也是将我们制定的元素存放到blockingQueue里面,如果说这个阻塞队列没有空间那么调用该方法的线程会阻塞等待

3)poll(time):取出BlockingQueue排在首位的元素,如果不能立即取出,那么会等到time规定的时间内取,规定时间到还没有取到那么直接返回null;

4)take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到Blocking有新的对象被加入为止

5)BlockingQueue不接受null 元素,试图add、put 或offer 一个null 元素时,某些实现会抛出NullPointerException,null 被用作指示poll 操作失败的警戒值。

  BlockingQueue queue1=new ArrayBlockingQueue(10);
  BlockingQueue queue2=new LinkedBlockingQueue(10);
  BlockingQueue queue3=new PriorityBlockingQueue<>(10);      

阻塞队列的原理:

在我们实现循环队列的时候,有一个重要的问题,如何判断使空队列,还是满的队列?

1)head==tail来进行判断,这是并不靠谱的

由于一直进行插入元素,导致的head==tail,就说明是队列满了

由于一直进行删除元素,导致的head==tail,就说明此时队列是空的

所以我们这么做:size=0就是空,size==数组长度就是满

在这里,必须要加一个锁对象,给谁加锁就锁哪一个对象

2)数组实现队列,就是一个循环队列,我们用[head,tail)这个范围来表示数组的一个有效元素范围

3)当我们的head或者tail到达数组元素的末尾之后,我们就需要从头开始,重新进行循环

阻塞队列---多线程_第1张图片

我们进行入队列:就是把新的元素放到tail位置上面,并且让tail++(元素不满)

我们进行出队列:就是把随手元素取出来,让head++(元素不为空)

之前我们在编写数据结构相关的代码的时候,

1)我们可以浪费一个格子,直接浪费,head==tail认为是空,head=tail+1认为是满

2)我们可以是用一个变量来进行记录元素的个数,size==0认为是空,size==array.length认为是满

数组实现队列,就是一个循环队列
入队列,就是把新的元素放到tail位置上,并且tail++;
出队列,就是把队首元素取出来,也就是说把head位置的元素返回回去,如果是引用数据类型,要手动置为空,并且head++;
class MyQueue
    {
//保存数据的本体
        Object object=new Object();
        private int []arr1;
//队首元素下标
        private int head=0;
//队尾元素下标
        private int tail=0;
//有效数据元素的个数
        private int count=0;
        MyQueue() {
            this.arr1 = new int[1000];
        }
        public void put(int data) {
            synchronized (object) {
                if (count == arr1.length) {
//此时队列中的值已经满了
//此时的条件,最好写成while
                    因为有可能会出现第一个线程放入元素后,第二个线程又继续放,就有会放满的情况,使用while的目的是为了让wait唤醒之后,再次去判断一下条件是否成立;
                    try{
                        object.wait();
                    }catch(InterruptedException e)
                    {
                        e.printStackTrace();
                    }
                }
                arr1[tail] = data;
                tail++;
//处于tail到达数组末尾的情况
                if (tail == arr1.length) {
                    tail = 0;
                }
//上面的这个条件判定可以写成tail=tail%array.length
                count++;
//我们put成功了,就可以进行唤醒take中的wait操作,因为此时队列一定是不为空的
                object.notify();
            }
        }
        public int take(){
            synchronized (object)
            {
                if (count == 0) {
//head==tail有一个元素,count=0一个元素都没有
                    try {
                        object.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                int ret = arr1[head];
                head++;
                if (head == arr1.length) {
                    head = 0;
                }
//我们take成功了,就可以唤醒put中的wait,因为此时队列一定不为满
                count--;
                object.notify();
                return ret;
            }
        }
    } 

1)我们要是想要这个队列支持线程安全,我们一定要保证在多线程环境下面调用这里面的put和take是没有任何问题的;

1.1)我们看了这里面的代码之后,put和take里面的每一行代码都是在操作公共的变量我们可以给整个方法来进行加锁或者是通过同步代码块的方式,指定this来加锁,或者指定一个专门的锁对象,锁这个对象和调用wait都是这个对象,所以说我们要是想保证线程安全,我们就需要进行使用synchronized来将若干个非原子性的操作,打包成原子性的操作

如果说我们要是想精准唤醒某一个线程,我们就需要使用不同的锁对象:

1)我们要是想唤醒t1,我们就必须o1.notify(),让我们的t1进行o1.wait()

2)我们要是想唤醒t2,我们就必须o2.notify(),让我们的t2进行o2.wait()

2)要想实现阻塞效果,我们就需要搭配对象等待集来进行使用

2.1)我们使用哪一个对象来进行加锁,我们就需要使用哪一个对象来进行wait操作,如果是针对this加锁就使用this.wait();

2.2)对于我们的put操作来说,阻塞条件就是队列为满

2.3)对于我们的take操作来说,阻塞条件就是队列为空

2.4)我们的put中的wait要靠take来进行唤醒,条件是队列为满,只要队列不为满,只要我们take成功取走了一个元素,队列不就不为满了吗,就可以唤醒了

2.5)我们take中的wait要靠put来进行唤醒,条件是队列为空,只要我们的队列不为空,也就是说只要我们put成功放进去了一个元素,队列不就不为空了吗,就可以唤醒了

2.6)当前代码中,我们的put操作和take操作两种操作不会同时wait,等待条件是截然不同的

2)注意:tail=tail%array.length这种写法十分的不建议

1)这种写法非常的不直观

2)取%操作,这一种操作对于计算机来说的开销是非常大的,相当于除法操作,比较操作就是一个跳转指令既不利于提高开发效率,也不能提高运行效率

我们使用阻塞队列来简单的写一些生产者消费者模型的代码:

阻塞队列出自于java.util.concurrent(并发)这个包,可以手动指定队列容量

1)生产者生产元素的速度是小于消费者消费的速度

2)put操作和take操作有可能都会出现阻塞的情况,但是此时由于这两个代码中的阻塞条件是对立的,因此我们两边的wait不会同时触发

put操作就会唤醒take的阻塞,put操作就破坏了take的阻塞条件

take操作就会唤醒put的阻塞,take操作也就破坏了put的阻塞条件

3)下面的if操作最好换成while操作,如果是多个线程出现阻塞等待的时候,万一同时唤醒了多个线程,就很有可能出现,第一个线程放入元素,第二个线程又放的时候,就会出现满的情况,所以我们使用while就是为了让wait被唤醒之后,再次确定一下条件是否成立

4)如果说有人等待,那么我们的notify是可以唤醒的,如果说没有人等待,那么notify没有任何副作用;

我们可以多创建几个线程作为生产者,或者多创建几个线程作为消费者

  public static void main(String[] args) {
            myqueue queue=new myqueue();//作为交易场所
            Thread t1=new Thread(){//搞一个这样的线程作为生产者
                public void run()
                {
                    for(int i=0;i<1000;i++)
                    {
                        try{
                            queue.put(i);
                            System.out.println("生产元素生产了"+i+"个");
                             sleep(1000);/每秒钟生产一个元素
                        }catch(InterruptedException e)
                        {
                           e.printStackTrace();
                        }
                    }
                }
            };
            t1.start();
        Thread t2=new Thread() {//搞一个这样的线程作为消费者
            public void run() {
                while (true) {
           //频繁取队首元素
                    int num = queue.take();
                    System.out.println("消费元素为" + num);
                }
            }

        };
        t2.start();
    }
}

生产者消费者模型,拿一个包饺子的例子来说

第一种方式:每一个人,擀完一个饺子一个包一个饺子,擀一个饺子包一个饺子,但是擀面杖只有一个,就会导致锁的冲突比较激烈,况且提高了门槛,我们只有先获取到这把锁才能进行擀饺子皮,其他人获取不到这把锁,就会阻塞等待,整体的效率并不会很高
第二种方式:一个人负责擀饺子,这个人擀出一堆皮,三个人负责取出这些皮,进行包饺子
1)生产者:擀皮的人;
2)消费者:包饺子的人;
3)交易场所:盖帘(放饺子皮的盖帘);

擀饺子皮的人就是饺子皮的生产者,要进行源源不断地生成饺子皮

包饺子的人就是及饺子皮的消费者,我们要不断地进行使用和消耗饺子皮

上述模型中一般生产者也只有一个,盖帘是交易场所,消费者确实有很多个

在计算机中,生产者就是一组线程,消费者是另一组线程,阻塞队列就是生产者消费者模型中的交易场所
阻塞队列---多线程_第2张图片

所以说在我们平时写代码的过程中,我们的代码一定要高内聚,低耦合

高内聚的意思就是说:我们希望不同模块之间,联系要尽量的少, 所以说我们使用生产者消费者模型就可以降低这里面的耦合

最大的用处解耦合:写了两个代码,一个代码中的两个代码块的关联关系很复杂,这样耦合就比较高,两个模块的关联关系尽量小,简单,整体的代码是可以相互理解的,耦合比较低

1)例如A要传输一定的数据给B,如果直接传输,此时就要求,要么是A向B传输数据,要么是B向A拉取数据,都是需要A和B进行相互交互的,A和B之间存在着一些关联关系,如果B挂了,A也就会有太大的影响;

2)在我们开发A代码的时候就必须充分了解B提供的一些接口,开发B代码的时候要充分了解A是怎么调用的;

3)未来如果需要进行扩展,扩展也搞一个C,让A也给C传输数据,这个改动就可能比较复杂,因为本来是A和B进行传输的,多了一个C,那么就是A想C传输数据,或者是C和B来向A拉取数据,改动比较复杂,就认为A和B的耦合比较高,B挂了,对A没啥影响

阻塞队列---多线程_第3张图片

1)A把数据写到队列里面,B再从队列里面取出元素进行消费,

2)A不知道数据要发送给谁,只需要向队列中添加元素,B不知道这个数据是谁发送过来的,只需要从队列中取元素即可

3)如果在后面需要进行扩展(再来C),也不需要直接从A要元素,只需要在队列中取出元素即可,这个阻塞队列就好似于变成了中转站一样的东西;
4)这样我们就做到了,让生产者和消费者可以不知道他们彼此之间是谁,这个数据是谁生产的,是谁消费的,都不重要,能生产,能消费就可以了,这样还是我们的系统变得更加灵活,可以随意替换A,B,C的任意一个模块,修改更方便,耦合耕地,让代码程序的维护性变得更高;

2)销峰填谷:
我们来举一个三峡大坝的例子
汛期:如果没有大坝,那么到了雨季,那么下游的水就会很大,就会造成水灾可能会发生灾难
罕期:如果没有大坝,那么下游的水就很少,有可能就会发生旱灾
于是就有了大坝:
1)汛期:关闸蓄水,并不是全部关了,让水按照一定的速率向下流,有节奏,按照一定的速率来进行放水,避免突然一波把下游带走(当突然下暴雨的时候);
2)罕期:开闸放水,让水也按照一定的速率向下流,避免下游太缺水,避免造成旱灾;

下面这种情况对于系统的稳定性是不利的

阻塞队列---多线程_第4张图片

 阻塞队列---多线程_第5张图片

3)首先我们让消费者消费得快一些,让生产者生产的慢一些,此时我们会看到,消费者线程会阻塞等待,每当有新的生产者的元素时,消费者才会执行;

4)但是如果消费者消费得慢一些,让生产者生产的快一些,就会出现,一开始大量的迸发出生产元素,等到生产了100多个才开始消费;(代码是第二种情况)

阻塞队列---多线程_第6张图片

1)互联网上面过来的请求数量,是多还是少,是不可控的;突然来了大量请求,如果没有入口服务器,这些什么商家服务器,直播服务器就有可能会挂掉,操作数据库,效率比较低,况且需要的系统资源可能会更多,如果主机的硬件不够,程序就有可能直接挂了,咱们的入口服务器一般是不会垮掉的,因为入口服务器是不会不会处理数据请求的

2)通过一个队列来进行缓存请求,建立一个生产者消费者模型,此时即使网络这边过来一大波请求,这些请求只是冲击了队列服务器,对于后续的业务服务器,仍然是按照固定的速率来消费数据,阻塞队列是没有什么计算量的,就单纯的存个数据,就能抗住更大的压力

3)实际上,我们的网管是不可以直接与各个服务器进行进行相连的,通过一个队列来实现生产者消费者模型

阻塞队列---多线程_第7张图片

1)现在即使说现在网络上面过来了一大波请求,此时这些请求指示冲击了队列服务器,但是对于后面的业务服务器,任然是以固定的速率来进行消费数据,如果互联网这边的请求少了,后面的这些服务器也不会闲着,就会把之前队列积压的数据,来取出来进行处理

2)我们这里此时的请求的压力直接给到了阻塞队列这里面,此时针对我们的队列的请求是暴涨的,但是我们的阻塞队列没有做过多的计算,没啥计算量,就是单纯的存储一个数据,它是可以承受住一定的压力的;

咱们在实际开发中使用到的阻塞队列并不是一个简单的数据结构,而是一组专门的服务器程序,它所提供的功能也并不仅仅是阻塞队列的功能,还会在这上面的队列中增加新的功能,比如说对于数据持久化存储,支持多个数据通道,支持多节点容灾冗余备份,支持管理面板,方便于配置参数,这样的队列我们又起了一个新的名字,叫做消息队列,但是本质上还是阻塞队列的功能,kafak,mq就是业界常见使用的消息队列

你可能感兴趣的:(链表,java,数据结构)