多线程之阻塞队列


前言

本篇介绍阻塞队列,通过阻塞队列实现生产者消费者模型, 再通过模拟实现阻塞队列,再次实现生产者消费者模型;如有错误,请在评论区指正,让我们一起交流,共同进步!



本文开始

1. 什么是阻塞队列?

阻塞队列:带有阻塞特性的队列
阻塞队列的特性:
① 如果队列为空,尝试出队列,会阻塞队列,等待到队列不为空为止;
② 如果队列满了,尝试入队列,会阻塞等待,等待到队列不满为止(不满了,就会入队);
【注】阻塞队列是线程安全的;

1.2 Java标准库提供的阻塞队列的使用

代码实现:

    public static void main(String[] args) throws InterruptedException {
        //BlockingDeque是一个接口,无法直接new实现,所以new 它的实现类:数组实现或者链表实现的阻塞队列
        BlockingDeque<String> queue = new LinkedBlockingDeque<>();
        //阻塞队列中的核心方法,注意两个:
        //被阻塞,就可能会被中断提前唤醒 =》此时需要抛出异常
        //put: 进队列
        queue.put("h1");
        queue.put("h2");
        //take:出队列
        String result = null;
        result = queue.take();
        System.out.println(result);
        result = queue.take();
        System.out.println(result);
    }

1.3 多线程使用阻塞队列 - 生产者消费者模型

生产者消费者模型 就是 可以使用阻塞队列;
生产者和消费者之间,交互数据,需要一个交易场所,这个场所相当于一个 阻塞队列(安全又带有阻塞功能);
例如:当生产者生产的不够消费者使用时,阻塞队列会产生阻塞;当生产者生产大于消费时,阻塞队列也会阻塞;
【注】生产者生产的都放到阻塞队列中,消费者消费从阻塞队列中取;队列满和队列空都产生阻塞;

生产者消费者模型,目的是什么?解决什么问题呢?
在这之前先来认识一下,内聚与耦合;
① 耦合:描述 两个模块之间 的关联关系的强弱的;关联越强,耦合越高;
② 内聚:在 一个模块 中,模块内部之间相关联的强弱;
低内聚:相关联的代码,没放到一起(乱放),认为关联程度低;
高内聚:相关联代码,合理的分类,各部分之间关联程度高;

生产者消费者模型解决的问题:
1.可以让上下模块之间,进行更好的 “解耦合”
示例:
A服务器 调用 B服务器 =》A给B发送请求,B给A返回响应;

在这里插入图片描述

    A与B之间直接通信,此时耦合就比较高,如果B挂掉, 对A也会有影响,A也会挂掉;
    如果再加入C服务器,此时 对A也有一个较大的调整;此时A,B,C直接的关联较大,这也体现了高耦合;

引入生产者消费者模型的目的:为了解决耦合高的问题;

前提:A服务器不知道B的存在,B服务器不知道A的存在,但是A,B都知道阻塞队列;
【注】A,B,C可以算是业务服务器,需要随时修改,支持新的需求功能;阻塞队列与业务无关,代码变化程度低,会更稳定一些;

图示:

在这里插入图片描述

解耦合达到的效果:
B服务器挂掉,不会影响到A服务器;再添加C服务器,直接让C服务器从阻塞队列中获取请求即可,对A服务器的影响较小;
【注】阻塞队列服务器也会挂掉,但是概率很小;

2.削峰填谷

前提:
服务器收到的请求数量,都是与用户行为相关的;
有些情况下,用户请求会出现 “峰值” ,爆发式涨一波;
假设有A,B两个服务器,A,B直接调用(A收到请求,B马上接收请求),此时A,B都会有一个请求峰值;

在这里插入图片描述

例如:
A服务器平时接受每秒1w请求,突然某个时间点每秒收到3w请求;此时B也接收相同的请求,相对于A,B来说都会到达一个峰值,但是如果B服务器没有考虑到峰值情况,此时B服务器就可能挂掉;

【注】服务器为什么会挂掉?
服务器在接收每个请求的时候,都会消耗硬盘资源,包含cpu,内存,硬盘等;当某个硬件资源到达峰值,不能再给请求分配资源,此时服务器就会挂掉;

解决方式:引入生产者消费者模型;
图示:削峰填谷:用户请求多了用阻塞队列削峰,用户请求少了用阻塞队列填谷,使用之前堆积的数据;(绿色字体时解释)

在这里插入图片描述

1.4 实现生产者消费者模型

创建两个线程t1,t2;t1作为生产者,t2作为消费者;生产一个消费一个;

代码实现:

public static void main(String[] args) {
        BlockingDeque<Integer> blockingDeque = new LinkedBlockingDeque<>();
        //t1作为消费者
        Thread t1 = new Thread( () -> {
            //不断的从队列中取元素,使用循环
            while (true) {
                //从队列中取元素
                try {
                    int value = blockingDeque.take();
                    System.out.println("消费元素:" + value);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        //t2作为生产者
        Thread t2 = new Thread( () -> {
            //生产元素
            int value = 0;
            while (true) {
                try {
                    System.out.println("生产元素:" + value);
                    blockingDeque.put(value);
                    value++;
                    //防止生产太快,使用sleep阻塞一下
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t2.start();
    }

代码结果:

在这里插入图片描述

1.5 模拟实现阻塞队列

1.实现普通队列
【注】BlockingQueue没有获取队列队首元素,不出队列(有peek的方法但不会阻塞);只要take去元素, put放元素;

代码实现:

class MyBlockingQueue {
    private int[] items = new int[1000];
    //队首head,队尾tail
    private int head = 0;
    private int tail = 0;
    private int size = 0;

    //入队列
    public void put(int elem) {
        //如果队列满了,插入失败
        if(size == items.length) {
            return;
        }
        items[tail] = elem;
        tail++;
        //万一tail到队列末尾,就需要让tail从头再来
        if(tail == items.length) {
            tail = 0;
        }
        //tail = tail % items.length;
        size++;
    }
    //出队列
    public Integer take() {
        //如果对列为空,返回空
        if(size == 0) {
            return null;
        }
        //取出对首操作
        int value = items[head];
        head++;
        //到最后一个位置
        if(head == items.length) {
            head = 0;
        }
        size--;
        return value;
    }
}

2.加上线程安全
根据上述,发现代码有很多修改操作,此时代码可能不是安全的;
为了保证线程安全:给每个方法加锁
多线程读的时候,size,head,value 可能发生内存可见性问题 - 加上volatile;

增加代码显示:
【注】这里的锁对象是this =》MyBlockingQueue ;

class MyBlockingQueue {
	//其他一样
    volatile private int head = 0;
    volatile private int tail = 0;
    volatile private int size = 0;
    synchronized public void put(int elem) {
     ...
    }
    synchronized public Integer take() {
     ... 
    }  

3.加上阻塞队列
①队列满了,就等待,等出队列操作里的唤醒操作;
②队列空了,就等待,等入队列操作里的唤醒;
代码实现:
【注】这里只显示增加的代码

class MyBlockingQueue {
	//.....与上述代码一样
	synchronized public void put(int elem) throws InterruptedException {
        if(size == items.length) {
            this.wait();
        }
        //....与上述代码一样
           this.notify();
    }
    synchronized public Integer take() throws InterruptedException {
        if(size == 0) {
            this.wait();
    }
        //....与上述代码一样
            this.notify();
        return value;
    }
}

1.5.1 模拟阻塞队列总代码

代码实现:
问题:到达队列末尾怎么在到头部?
① 重置tail = 0 ;② 使用 tail = tail % items.length ;
使用②这个求余代码的执行效率和开发效率都不会提高,一般不建议使用;

class MyBlockingQueue {
    private int[] items = new int[1000];
    //约定[head, tail]队列的有效元素
    //队首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 {
        //如果队列满了,插入失败
        if(size == items.length) {
            this.wait();
//            return;
        }
        items[tail] = elem;
        tail++;
        //万一tail到队列末尾,就需要让tail从头再来
        if(tail == items.length) {
            tail = 0;
        }
        size++;
        this.notify();
    }
    //出队列
    synchronized public Integer take() throws InterruptedException {
        //如果对列为空,返回空
        if(size == 0) {
            this.wait();
            //return null;
        }
        //取出对首操作
        int value = items[head];
        head++;
        //到最后一个位置
        if(head == items.length) {
            head = 0;
        }
        size--;
        this.notify();
        return value;
    }
}

产生Bug问题:
wait()方法可能被其他方法中断,例如interrupt方法;
此时它的等待条件还没满足,就提醒唤醒,此时可能产生bug;

解决方式:
使用循环判断两次:①在条件没满足的情况下,开始wait(); ②等到被唤醒,再次判断条件是否满足,不满足还可以继续wait();

给判定条件增加代码:

    synchronized public void put(int elem) throws InterruptedException {
    	//...
        while (size == items.length) {
            this.wait();
        }
        //...
	}
	 synchronized public Integer take() throws InterruptedException {
	   //...
        while (size == 0) {
            this.wait();
        }
       //...
	}

1.5.2 更新版本模型实现阻塞队列实现生产者消费者模型代码

代码实现:

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();
        }
        items[tail] = elem;
        tail++;
        //万一tail到队列末尾,就需要让tail从头再来
        if(tail == items.length) {
            tail = 0;
        }
        size++;
        this.notify();
    }
    //出队列
    synchronized public Integer take() throws InterruptedException {
        //如果对列为空,返回空
        while (size == 0) {
            this.wait();
            //return null;
        }
        //取出对首操作
        int value = items[head];
        head++;
        //到最后一个位置
        if(head == items.length) {
            head = 0;
        }
        size--;
        this.notify();
        return value;
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        MyBlockingQueue myBlockingQueue = new MyBlockingQueue();
        //消费者
        Thread t1 = new Thread( () -> {
           while (true) {
               try {
                   int value = myBlockingQueue.take();
                   System.out.println("消费元素:" + value);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        //生产者
        Thread t2 = new Thread( () -> {
            int value = 0;
           while (true) {
               try {
                   System.out.println("生产的元素" + value);
                   myBlockingQueue.put(value);
                   Thread.sleep(1000);
                   value++;
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        t1.start();
        t2.start();
    }
}

总结

✨✨✨各位读友,本篇分享到内容如果对你有帮助给个👍赞鼓励一下吧!!
感谢每一位一起走到这的伙伴,我们可以一起交流进步!!!一起加油吧!!!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值