java多线程案例-阻塞队列(生产者消费者模型)

阻塞队列

一、认识阻塞队列

1.什么是阻塞队列

阻塞队列是一种特殊的队列,遵守 “先进先出” 的原则,并且是一种线程安全的数据结构。

阻塞队列的特性:

  • 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素。
  • 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素。

阻塞队列的一个典型应用场景就是 “生产者消费者模型”。

2.生产者消费者模型

image-20230325105612977

以擀面皮+包饺子为例:

  • 每个人独立完成擀面皮+包饺子的全流程,假设只有一个擀面杖,但是有多个人要包饺子,这样就会存在有人等待擀面杖的情况,导致效率下降。
  • 一个人专门负责擀饺子皮, 另外的人负责包, 擀饺子的人每次擀好一个皮, 就放到装饺子皮的某一件东西上面, 其他人直接取饺子皮进行包饺子。【生产者:擀饺子皮的人;消费者:包饺子的人】
  • 如果面皮已经有很多了,擀面皮的人就等一会包饺子的人;如果面皮不够用,那么包饺子的人就等一会擀面皮的人。

3.为什么要使用阻塞队列

场景一:

假设有两个服务器A(请求服务器),B(应用服务器),如果A,B直接传递消息,而不通过阻塞队列,那么当A请求突然暴涨的时候,B服务器的请求也会跟着暴涨,由于B服务器是应用服务器,处理的任务是重量级的,所以该情况B服务器大概率会挂。

image-20230325103543691

场景二:

如果使用生产者消费者模型,那么即使A请求暴涨,也不会影响到B,顶多A挂了,应用服务器不会受到影响,这是因为A请求暴涨后,用户的请求都被打包到阻塞队列中,B还是以相同的速度处理这些请求,所以生产者消费者模型可以起到削峰填谷的作用。

image-20230325104016930

削峰填谷

image-20230325104853393

把请求高峰部分削掉,填补到请求低谷部分,从而使整个过程看起来趋于平缓

二、实现生产者消费者模型

基于标准库的阻塞队列简单实现的生产者消费者模型

public class ThreadDemo20 {
    public static void main(String[] args) {
        BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();

        // 消费者
        Thread t1 = new Thread(() -> {
            while (true) {
                try {
                    int value = blockingQueue.take();
                    System.out.println("消费元素: " + value);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();

        // 生产者
        Thread t2 = new Thread(() -> {
            int value = 0;
            while (true) {
                try {
                    System.out.println("生产元素: " + value);
                    blockingQueue.put(value);
                    value++;
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t2.start();

        // 上述代码, 让生产者, 每隔 1s 生产一个元素.
        // 让消费者则直接消费, 不受限制.

    }
}

代码效果:生成者每生成一个元素,消费者消费一个元素,如果队列没有元素,消费者就阻塞等待生产者。

GIF 2023-3-25 13-48-05

三、实现阻塞队列

阻塞队列实际上就是循环队列,而循环队列有两种实现方式

  • 用一个变量记录队列元素个数【使用这种方式实现】

  • 浪费一个空间实现

1.实现循环队列

力扣链接

假设数组长度为8,size=0表示队列为空,size=8表示队列满

image-20230325121440937

插入元素

image-20230325121341463

删除元素

image-20230325121314878

如果rear到了最后一个位置,插入了元素之后,rear就应该指向第一个位置,所以不能一味的用rear++和font++

应该使用**(rear+1)%数组长度(front+1)%数组长度**

image-20230325121626164

代码实现

//循环队列
public class MyCircularQueue {
    //队列数据
    private int[] elems ;
    //队头指针
    private int front;
    //队尾指针
    private int rear;
    //队列元素个数
    private int size;

   public MyCircularQueue(int k) {
        elems = new int[k];
    }

    //出队
    public boolean deQueue() {
        if (isEmpty()) {
            //队列为空
            return false;
        }
        int ret = elems[front];
        front =(front+1)%elems.length;
        size--;
        return true;
    }

    //入队
    public boolean enQueue(int elem) {
        if (isFull()) {
            //队列满
            return false;
        }
        elems[rear] = elem;
        rear = (rear+1)%elems.length;
        size++;
        return true;
    }
    //获取队头元素
    public int Front() {
        if(isEmpty()) {
            return -1;
        }
        return elems[front];
    }
    //获取对尾元素
    public int Rear() {
        if(isEmpty()) {
            return -1;
        }
        int index = (rear == 0) ? elems.length-1 : rear-1;
        return elems[index];
    }
    //是否为空队列
    public boolean isEmpty() {
        return size==0;
    }
    //是否满队列
    public boolean isFull() {
       if( size == elems.length) {
            return true;
        }
        return false;
    }
}

2.实现阻塞队列

  • 由于入队和出队都有写操作,所以我们避免线程的不安全,进行上锁处理

      synchronized public Integer take() {
    
        }
    
        //入队
       synchronized public void put(int val) {
    
        }
    
  • put和take两个方法都会读取变量,所以我们用volatile修饰变量,避免内存可见性

    volatile private int front = 0;
    volatile private int rear = 0;
    volatile private int size = 0;
  • 入队时,队列为满需要使用wait方法使线程阻塞,直到有旧元素出队才使用notify通知线程执行。
    出队时,队列为空需要使用wait方法使线程阻塞,直到有新元素入队才使用notify通知线程执行。
    // 入队列
    synchronized public void put(int elem) throws InterruptedException {
        while (size == items.length) {
            // 队列满了, 阻塞等待
            this.wait();
        }
        items[rear] = elem;
         rear = (rear+1) % items.length;
         size++;
        //唤醒出队列的wait
        this.notify();
    }

    // 出队列
    synchronized public Integer take() throws InterruptedException {
        while (size == 0) {
         // 队列空了, 阻塞等待.
            this.wait();
        }
        int value = items[front];
        front = (front+1) % items.length;
        size--;
        //唤醒入队列的wait
        this.notify();
        return value;
    }

image-20230325140237081

  • 入队和出队的wait不会同时发生
  • 即使都没有wait,执行了notify并不会产生影响,类似投篮投了但没有进。
  • 使用 while (size == items.length) 而不是直接if判断:
    • wait可能被其他方法唤醒(interrupt),导致程序无法正常运行

    • 解决办法:在wait唤醒之后,再确认一下条件是否满足。(比如入队列的wait,被唤醒后再次判断队列是否满,如果是满了,就继续wait)

最终版

class MyBlockingQueue {
    private int[] items = new int[1000];
    volatile private int front = 0;
    volatile private int rear = 0;
    volatile private int size = 0;

    // 入队列
    synchronized public void put(int elem) throws InterruptedException {
        while (size == items.length) {
            // 队列满了, 阻塞等待
            this.wait();
        }
        items[rear] = elem;
         rear = (rear+1) % items.length;
         size++;
        //唤醒出队列的wait
        this.notify();
    }

    // 出队列
    synchronized public Integer take() throws InterruptedException {
        while (size == 0) {
         // 队列空了, 阻塞等待.
            this.wait();
        }
        int value = items[front];
        front = (front+1) % items.length;
        size--;
        //唤醒入队列的wait
        this.notify();
        return value;
    }
}

3.测试阻塞队列

情况1:生产者生产与消费者消费的频率一致

public static void main(String[] args) {
        MyBlockingQueue queue = new MyBlockingQueue();
        // 消费者 每1s消费一个
        Thread t1 = new Thread(() -> {
            while (true) {
                try {
                    int value = queue.take();
                    System.out.println("消费: " + value);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 生产者 每1s生产一个
        Thread t2 = new Thread(() -> {
            int value = 0;
            while (true) {
                try {
                    System.out.println("生产: " + value);
                    queue.put(value);
                    value++;
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        t2.start();
    }

运行结果:

GIF 2023-3-25 14-13-38

情况2:生产者生产频率比消费者消费的频率更快

    public static void main(String[] args) {
        MyBlockingQueue queue = new MyBlockingQueue();
        // 消费者
        Thread t1 = new Thread(() -> {
            while (true) {
                try {
                    int value = queue.take();
                    System.out.println("消费: " + value);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 生产者
        Thread t2 = new Thread(() -> {
            int value = 0;
            while (true) {
                try {
                    System.out.println("生产: " + value);
                    queue.put(value);
                    value++;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        t2.start();
    }

运行结果:GIF 2023-3-25 14-20-11

由于生产者没有sleep,所以很快就生产满了,之后就需要等着消费者每消费一个,才能生产一个。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值