【多线程】手把手教会你阻塞队列原理以及如何实现生产者消费者模型 超详细!!

在这里插入图片描述

大家好呀
我是浪前

今天给大家讲解的是**阻塞队列及生产者消费者模型**
祝愿所有点赞关注的人,身体健康,一夜暴富,升职加薪迎娶白富美!!!
点我领取迎娶白富美大礼包

🍎定义:

阻塞队列是多线程中的常用的数据结构

🍎特点:

阻塞队列是一种特殊的队列,具有的特点如下:

  1. 涉及到线程安全问题
  2. 带有阻塞特性
  3. 若队列为空,继续出队,会发生阻塞,阻塞到其他线程向队列中添加元素为止
  4. 若队列为满,继续入队,会发生阻塞,阻塞到其他线程向队列中取走元素为止

注意:
空队列出队阻塞,直到加元素为止
满队列入队阻塞,直到出元素为止

🍎阻塞队列的作用:

阻塞队列可以用来实现生产者消费者模型

生产者消费者模型是一种常见的多线程的代码的编写方式

🍎生产者消费模型

生产者和消费者借助于这个阻塞队列来进行交互
生产者把生产出来的内容放到阻塞队列中
消费者就从阻塞队列中获取内容

如果生产者生产得慢,那么消费者就得等
因为消费者从空的队列中获取元素就会阻塞

如果生产者生产得快,那么生产者就得等
因为生产者向满的队列中添加元素也会阻塞

作用:

阻塞队列可以降低资源竞争,提高执行效率

🍎阻塞队列的优势:

1.解耦合:

耦合

两个模块的联系越紧密耦合度就越高

解耦合对于一个分布式系统来说是更加有意义的

客户端通过网络传递到服务器

分布式系统没有加阻塞队列之前:
如图:
在这里插入图片描述

上面的耦合度太高了
那么如何去降低这个耦合度?
我们就可以使用一个阻塞队列来降低这个耦合度

加了阻塞队列之后:
在这里插入图片描述

除了解耦合之外,阻塞队列还有一个优势:;

2.削峰填谷

: 短时间内请求量较多
: 短时间内请求量较少

没有加入阻塞队列的分布式:
在这里插入图片描述

加入了阻塞队列的分布式:
在这里插入图片描述

虽然加入了消息队列之后,会有一定的请求在消息队列中挤压
会使得A得到的响应速度会变慢,但是也好过完全没有响应

而且上述的高峰值情况不会一直出现,只会短时间出现
等到过了峰值之后 ,服务器A的请求量就恢复正常了
而服务器B就可以逐渐地把积压的数据都处理掉
上述的过程就是削峰填谷

销峰:当高峰值来了之后,我们通过队列把这个高峰值给抗住,保证服务器B不会一下子挂掉

填谷:当高峰过了之后,请求量没有那么多了之后,我们就让服务器B慢慢地处理掉积压在队列中的数据

作用:

通过消息队列让服务器之间的交互变得更加的灵活
即使两个服务器之间的步调不一致,也可以通过消息队列来协调

所以有了削峰填谷的机制之后,我们就可以保证在突发情况来临的时候,整个服务器系统仍然可以正确执行

就像是中国的三峡大坝一样

🍎阻塞队列的实现:

在我们的java标准库中已经提供了现成的阻塞队列让我们直接使用

public static void main(String[] args) {  
    BlockingDeque<String> queue = new LinkedBlockingDeque<>();  
      
}

Queue这里提供的各种方法
对于BlockingQueue来说也可以使用

但是一般不建议使用这些Queue中的方法
因为这些方法都不具备“阻塞”特性

所以我们就要使用专门带有阻塞特性的方法

put 方法:阻塞式的入队列
take 方法: 阻塞式的出队列

但是没有提供可以实现阻塞式的获取队首元素的方法

例子如下:

public class Text{  
    public static void main(String[] args) throws InterruptedException {  
        BlockingDeque<String> queue = new LinkedBlockingDeque<>();  
        queue.put("111");  
        queue.put("123");  
        queue.put("233");  
        String elem = queue.take();  
        System.out.println(elem);  
        elem = queue.take();  
        System.out.println(elem);  
        elem = queue.take();  
        System.out.println(elem);  
        elem = queue.take();  
        System.out.println(elem);  
    }  
}

代码运行结果:
在这里插入图片描述

通过刚刚的代码例子
我们了解了标准库中的阻塞队列如何使用
同时更重要的是我们还要能够自己去实现一个阻塞队列

阻塞队列的实现步骤:

一个普通的队列可以基于数组/链表
就是基于一个普通的队列,加上线程安全,加上阻塞,就可以了

🍎实现普通数组队列

基于数组实现的队列:
在这里插入图片描述

当队列初始化时:队列为空,head和tail重合的

当队列满了的时候:head和tail也是重合的

那么我们该如何去区分?
专门搞一个变量size来表示元素个数
size= 0则队列为空size为数组最大值则队列是满的

🍎初始化数组队列的代码:

代码如下:

class MyBlockingQueue{  
    private String[] data = new String[1000];  
    //队列的起始位置  
    private int head = 0;  
    //队列的结束位置的下一个位置  
    private int tail = 0;  
      
    //队列中的元素个数  
    private int size = 0;  
      
    //提供核心方法  
    //入队列:  
    public void put(String elem){  
        if(size == data.length){  
            return ;  
        }  
          
        data[tail] = elem;  
        tail++;  
        //如果tail++后到达了数组的末尾,此时就要让tail回到开头  
        if(tail == data.length){  
            tail = 0;  
        }  
        size++;  
         
    }  
      
    //出队列:  
    public  String take(){  
        //队列为空直接返回  
        if(size == 0){  
            return null;  
        }  
          
        //删除队首元素,进行返回  
        String ret = data[head];  
        head++;  
        if(head == data.length){  
            return null;  
        }  
        size--;  
        return ret;  
    }  
      
}  
public class Demo06 {  
    public static void main(String[] args) {  
          
    }  
}

上面是一个普通的由数组构成的队列

🍎实现阻塞队列

下面我们将在刚刚的代码上进行改进,修改为阻塞队列

这个队列引入线程安全,就要进行加锁:

加锁方式:

把入队的方法和出队的方法都全部加上锁:

代码如下:

class MyBlockingQueue{  
    private String[] data = new String[1000];  
    //队列的起始位置  
    private int head = 0;  
    //队列的结束位置的下一个位置  
    private int tail = 0;  
  
    //队列中的元素个数  
    private int size = 0;  
      
    //创建一个锁:  
    public final Object locker = new Object();  
  
    //提供核心方法  
    //入队列:  
    public void put(String elem){  
        //给put方法全加上锁:  
        synchronized (locker){  
            if(size == data.length){  
                return ;  
            }  
            data[tail] = elem;  
            tail++;  
            //如果tail++后到达了数组的末尾,此时就要让tail回到开头  
            if(tail == data.length){  
                tail = 0;  
            }  
            size++;  
        }  
    }  
  
    //出队列:  
    public  String take(){  
        //给take方法全部加上锁:  
        synchronized (locker){  
            //队列为空直接返回  
            if(size == 0){  
                return null;  
            }  
  
            //删除队首元素,进行返回  
            String ret = data[head];  
            head++;  
            if(head == data.length){  
                return null;  
            }  
            size--;  
            return ret;  
        }  
    }  
  
}  
public class Demo06 {  
    public static void main(String[] args) {  
  
    }  
}

阻塞的实现方式:

阻塞是使用wait和notify来实现:

🍎put方法中的阻塞:

在put方法中如果队列已经满了
此时若继续向队列中添加新的元素就会导致队列阻塞
此时我们就在put方法中使用wait方法来实现这个阻塞:

put方法中的阻塞实现:

if(size == data.length){
//如果是队列满,继续加入元素就会阻塞
locker.wait();
}

take方法中的notify来唤醒put方法中的wait阻塞:

put方法会一直阻塞到调用take方法为止
此时就可以在take方法中使用notify方法来唤醒:

//这个notify用来唤醒put方法中的wait方法
locker.notify();

🍎take方法中的阻塞:

在take方法中,如果队列已经为空了
此时继续删除元素就会发生阻塞

take方法中:

//队列为空,继续删除元素会阻塞
if(size == 0){
locker.wait();
}

此时take方法中的阻塞我们在put方法中进行唤醒
take方法中:

//这个notify方法用来唤醒take方法中的wait
locker.notify();

所有代码如下:

class MyBlockingQueue{  
    private String[] data = new String[1000];  
    //队列的起始位置  
    private int head = 0;  
    //队列的结束位置的下一个位置  
    private int tail = 0;  
  
    //队列中的元素个数  
    private int size = 0;  
  
    //创建一个锁:  
    public final Object locker = new Object();  
  
    //提供核心方法  
    //入队列:  
    public void put(String elem) throws InterruptedException {  
        //给put方法全加上锁:  
        synchronized (locker){  
            if(size == data.length){  
               //如果是队列满,继续加入元素就会阻塞  
                locker.wait();  
            }  
            data[tail] = elem;  
            tail++;  
            //如果tail++后到达了数组的末尾,此时就要让tail回到开头  
            if(tail == data.length){  
                tail = 0;  
            }  
            size++;  
  
            //这个notify方法用来唤醒take方法中的wait  
            locker.notify();  
        }  
    }  
  
    //出队列:  
    public  String take() throws InterruptedException {  
        //给take方法全部加上锁:  
        synchronized (locker){  
            //队列为空,继续删除元素会阻塞  
            if(size == 0){  
                locker.wait();  
            }  
  
            //删除队首元素,进行返回  
            String ret = data[head];  
            head++;  
            if(head == data.length){  
                return null;  
            }  
            size--;  
            //这个notify用来唤醒put方法中的wait方法  
            locker.notify();  
            return ret;  
        }  
    }  
  
}

put方法和take方法是相互唤醒的;

注意事项:

一个阻塞队列只有在要么是空,要么是满的时候才会出现阻塞,
所以take和put只有一边可以阻塞,
如果put阻塞了,其他线程继续调用put也都会阻塞,只能靠take唤醒

如果是take阻塞了,其他线程继续调用take也还是阻塞,只有靠put才能唤醒

当我们put方法因为队列满了,进入wait阻塞等待之后

一会儿wait返回,结束了wait的阻塞,此时的队列可能还是满的队列

因为wait除了notify之外,还有一个interrupt方法也是可以唤醒这个wait的
因为这个interrupt方法是可以中断wait的状态的

但是使用interrupt唤醒的时候会出现一个异常:InterrupttedException

在当前的代码中,如果是interrupt方法唤醒了wait,同时还使用了throws 抛出了异常
那么直接整个方法就结束了,不会出现问题。

但是若使用的是try catch来抛出异常就会出问题了

if(size == data.length){  
    try {  
        this.wait();  
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    }  
}

如果我们使用如上代码抛出异常,那么当出现异常的时候,方法就不会结束了
会继续向下执行下面的代码:

data[tail] = elem;  
tail++;  
if(tail == data.length){  
    tail = 0;  
}  
size++;

此时,就会把tail指向的元素给覆盖掉,实际上此处队列还是满着的
此时tail指向的元素并非是无效元素,那我们这里就是把一个有效元素给覆盖掉了

而且size加一之后也会比数组的最大长度还要大, 此时肯定是不合理的

🍎使用wait的注意事项:

我们使用wait的时候一定要注意:
有两种情况出现:

wait被唤醒的时候,如果是通过notify来唤醒的
那么说明此时有其他线程调用了take方法
此时队列中已经不满了,就可以继续添加元素了
没有任何问题。

但是如果wait是通过Interrupt来唤醒的
那么此时队列还是满着的
就不可以添加元素了
如果继续添加元素,就会出问题

🍎如何解决:

解决原理:

如果本来是因为队列满了,wait从而进入了阻塞
那么在解除阻塞之后,还需要再去确认一次,看看队列是否还是满的
若队列还是满着的,说明没有调用take方法
刚刚的wait方法是通过Interrupt方法来唤醒的
此时就需要继续进行wait等待

代码实现:
在put方法中写出如下代码:
期望wait在被唤醒之后还去判定一次,看看队列满不满
如果队列不满就向后执行代码
如果队列还是满的,那么就继续阻塞等待

synchronized (this){  
    while (size == data.length){  
        try {  
            this.wait();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
  
    }

在take方法中写出如下代码:

while (size == 0){  
    this.wait();  
}

我们直接在使用wait的时候,使用while作为条件判定的方式
目的就是为了让wait唤醒之后还能再确认一次, 是否条件仍然满足
如果条件还是满足就继续阻塞,不满足就跳出循环

同时我们还记得要给这些变量加上volatile关键字:

因为后面的代码中的这些变量里有的要进行读(判定),有的要进行写(修改)
加上volatile关键字为了防止内存可见性:

private volatile String[] data = new String[1000];  
//队列的起始位置  
private volatile  int head = 0;  
//队列的结束位置的下一个位置  
private volatile  int tail = 0;  
  
//队列中的元素个数  
private  volatile  int size = 0;

🍎阻塞队列完整代码:

完整的阻塞队列的实现代码如下:

public class Demo20 {  
    private volatile  int tail = 0;  
    private volatile int size = 0;  
    private volatile int head = 0;  
    private volatile String[] data = new String[1000];  
    public void put(String elem) throws InterruptedException {  
        synchronized (this){  
            while (size == data.length){  
                this.wait();  
            }  
            data[tail] = elem;  
            tail++;  
            if(tail == data.length){  
                tail = 0;  
            }  
            size++;  
            this.notify(); //唤醒take的wait  
        }  
    }  
  
    public String take() throws InterruptedException {  
        synchronized (this){  
            while (size == 0){  
                this.wait();  
            }  
            String ret = data[head];  
            head++;  
            if(head == data.length){  
                head = 0;  
            }  
            size--;  
            this.notify();//唤醒put中的阻塞  
            return ret;  
        }  
    }  
}

🍎生产者消费者模型的实现:

借助上述的阻塞队列,来实现一个简单的生产者消费者模型

实现步骤:

  1. 生产者和消费者都分别使用一个线程来表示(也可以使用多个线程来表示)

  2. 而且在生产者和消费者之间还需要一个阻塞队里来协调:

🍎生产者:

生产者:

//生产者
Thread t1 = new Thread(() ->{

});

实现细节

  1. 使用while循环来向队列中添加元素
  2. 添加完毕之后sleep500毫秒
Thread t1 = new Thread(() ->{  
    int num = 1;  
    while(true){  
        try {  
            queue.put(num+"");  
            System.out.println("生产元素"+ num);  
            num++;  
            Thread.sleep(500);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  
});

消费者:

//消费者
Thread t2 = new Thread(() ->{
});

实现细节

使用循环向队列中删除元素

//消费者  
Thread t2 = new Thread(() ->{  
    while(true){  
        try {  
            String result = queue.take();  
            System.out.println("消费元素" + result);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
  
    }  
});

阻塞队列:

MyBlockingQueue queue = new MyBlockingQueue();

以上代码实现结果如下:

public class Demo06 {  
    public static void main(String[] args) {  
        MyBlockingQueue queue = new MyBlockingQueue();  
        //生产者  
        Thread t1 = new Thread(() ->{  
            int num = 1;  
            while(true){  
                try {  
                    queue.put(num+"");  
                    System.out.println("生产元素"+ num);  
                    num++;  
                    Thread.sleep(500);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        });  
  
  
        //消费者  
        Thread t2 = new Thread(() ->{  
            while(true){  
                try {  
                    String result = queue.take();  
                    System.out.println("消费元素" + result);  
                      
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
  
            }  
        });  
        t1.start();  
        t2.start();  
    }  
}

在这里插入图片描述

生产者每次生产一个元素就休眠500毫秒
生产者就生产得慢,消费者此时就将就着生产者

刚刚是生产者休眠500毫秒,下面是消费者休眠500毫秒:

public class Demo06 {  
    public static void main(String[] args) {  
        MyBlockingQueue queue = new MyBlockingQueue();  
        //生产者  
        Thread t1 = new Thread(() ->{  
            int num = 1;  
            while(true){  
                try {  
                    queue.put(num+"");  
                    System.out.println("生产元素"+ num);  
                    num++;  
  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        });  
  
  
        //消费者  
        Thread t2 = new Thread(() ->{  
            while(true){  
                try {  
                    String result = queue.take();  
                    System.out.println("消费元素" + result);  
                    Thread.sleep(500);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
  
            }  
        });  
        t1.start();  
        t2.start();  
    }  
}

结果如下:
在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值