赞
踩
目录
队列:先进先出
阻塞队列:就是带有阻塞特性的队列,主要概念如下:
1.如果队列为空,尝试出队列,就会阻塞等待,等待到队列不为空为止;
2.如果队列未满,尝试入队列,也会阻塞等待,等待到队列不满为止。
3.阻塞队列是线程安全的。
当我们写多线程代码时,多个线程之间进行数据交互,就可以使用阻塞队列简化代码编写。
BlockingDeque<String> queue = new LinkedBlockingDeque<>();
这里的BlockingDeque是一个接口,所以不能直接new。
阻塞队列的核心方法:
这里入了五个元素。
- queue.put("hello1");
- queue.put("hello2");
- queue.put("hello3");
- queue.put("hello4");
- queue.put("hello5");
将前面五个元素出队列,当前面五个队列都出去了,队列为空了,此时队列发生阻塞。
- //2.take出队列
- String result = null;
- result = queue.take();
- System.out.println(result);
- result = queue.take();
- System.out.println(result);
- result = queue.take();
- System.out.println(result);
- result = queue.take();
- System.out.println(result);
- result = queue.take();
- System.out.println(result);
- result = queue.take();//发生阻塞,无法打印
- System.out.println(result);
关于这个模型,举例说明如下:
假如有几个人在包饺子,这几个人发现,擀面杖只有一个,如果每个人边擀面边包饺子太慢,还要争抢擀面杖,于是他们就做了如下改动:
此时,我们就将1号比喻为生产者(产生资源),包饺子的另外几个人就是消费者(消耗资源),这个盖帘就是阻塞队列(放置资源)。
所以结合阻塞队列的特性:
盖帘放不下了(队列满了),1号就暂时不放了(阻塞等待,就暂时不入队列了);
盖帘空了(队列空了),包饺子的人就可以歇息(阻塞等待,暂时不出队列了)。
高耦合:联系紧密,关系紧密,如果两者之间符合高耦合,乙方有什么变化,可能就会影响到另一方。低耦合则是与之相反。
内聚:就是指有关联的东西放在一起。
考虑到以下场景:
A服务器调用B服务器:A给B发送请求,B给A返回响应,两个服务器正在进行正常交互,两者之间属于高耦合的情况,如果A或者B出现问题,就会对双方出现影响。
然后此时来了一个C,也要于A进行交互,
此时A如果要与C进行交互,A就需要做一个很大的调整,但此时就会影响到B,针对以上的问题我们就需要引入“生产者消费者模型”,用到阻塞队列。
此时A,B,C相互不知道对方的存在,如果需要交互资源,直接通过阻塞队列服务器获取即可,相互不会受到影响,起到了一个“解耦合”的效果。
关于阻塞队列服务器,其实也有可能出问题,但是比ABC出现问题的概率要低,因为我们做业务,一般实在ABC里面进行,容易出bug,而不用阻塞队列,所以阻塞队列不容易出问题。
举例以下情况,用户发出请求,AB两个服务器是相互调用的关系,如下图,
峰值:比如A平时受到1万/秒的请求,然后突然出现了3万/秒的请求,这种情况就会出现峰值。
如果A出现峰值,那么B也会出现峰值。如果此时B没有考虑峰值的处理,由于服务器处理每个请求,都要消耗一定的硬件资源(CPU,内存......),那么这个B就会出现问题,这种情况,就会给系统的稳定性带来一定的风险。
所以以上直接调用的方式就会给系统稳定性带来风险,所以就需要运用“阻塞队列”进行“削峰填谷”。
如下图,
虽然A收到的请求多了,但由于每次是A,B之间不是直接交互,有了阻塞队列,就是阻塞队列里面的元素增多了,但是B还是按照原来的速率从阻塞队列里拿取请求,不会受到影响,阻塞队列帮助B承担了压力,此过程即为“削峰”。
当峰值过去之后,还有一个波谷,但是由于B有了阻塞队列,也就不用改变畜栏里请求的速率,仍按照原来的速率消耗阻塞队列中的元素,此过程即为“填谷”。
代码作用:
生产者:每隔1s产生一个元素
消费者:直接消费,不受限制。
代码实现如下:
- public static void main(String[] args) {
- //创建一个阻塞队列
- BlockingDeque<Integer> blockingDeque = new LinkedBlockingDeque<>();
-
- //消费者
- Thread t1 = new Thread(() -> {
- while(true) {
- int value = 0;
- try {
- value = blockingDeque.take();
- System.out.println("消费元素:" + value);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- });
- t1.start();
- //生产者
- Thread t2 = new Thread(() -> {
- int value = 0;
- while (true) {
- System.out.println("生产元素:" +value);
- try {
- blockingDeque.put(value);
- value++;
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- });
- t2.start();
- }
队列空:head和tail重合
队列满:当tail < head时,就视为队列满
基于队列有很多的读入和修改操作,此时我们需要使用synchronized和volatile关键字。
阻塞机制:
(1)队列满了就wait(),然后队列有元素出去了,不满了,就让notify()唤醒;
(2)队列空了就wait(),当队列增加了元素也让notify()唤醒。
此处有一个注意点,wait()有可能会被提前唤醒,很多方法,比如使用interrupt,就会把wait()提前还行,但是此时可能条件还没满足(还没满或者还没非空),wait就直接被唤醒往下走了,就有可能会出现问题。
所以我们就需要在wait唤醒以后,再加一个判定条件:
wait()之前,发现条件不满足,开始wait();然后等到wait()被唤醒了以后,再确认以下这个条件是不是满足的,如果不满足,还是再继续wait()
- class MyBlockingQueue {
- private int[] items = new int[1000];
- //约定[head,tail]队列的有效元素
- volatile private int head = 0;//指向队首元素的下标
- volatile private int tail = 0;//指向队尾元素的下标
- volatile private int size = 0;//获取队列种的元素个数
-
- //入队列
- synchronized public void put(int elem) throws InterruptedException {
- while (size == items.length) {
- //队列满了,插入失败
- this.wait();//队列满了就要阻塞
- }
- //把新元素放到tail所在的位置上
- items[tail] = elem;
- tail++;
- //万一tail达到末尾,就需要让tail从头再来,达到一个循环的效果
- if (tail == items.length) {
- tail = 0;
- }
- //tail = tail % items.length;//求模也可以表示循环
- size++;
- this.notify();//唤醒空的队列
- }
- //出队列
- synchronized public Integer take() throws InterruptedException {
- while (size == 0) {
- //return null;
- this.wait();//队列空了就要阻塞
- }
- int value = items[head];
- head++;
- if (head == items.length) {
- head = 0;
- }
- size--;
- this.notify();//唤醒满的队列
- return value;
- }
- }
阻塞队列到这里就结束了,求一键三连啦~~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。