Java 多线程案例之生产者-消费者模型以及阻塞队列的实现

Java 多线程案例之阻塞队列及模拟实现



一、什么是阻塞队列?

阻塞队列(Blocking Queue)是一个支持先进先出的队列,同时是一个支持阻塞操作的队列,即:

  • 当队列满时,会阻塞执行入队列操作的线程,直到队列不为满,直至队列不满;
  • 当队列为空时,会阻塞执行出队列操作的线程,直至队列不为空。

阻塞队列用在多线程的场景下,因此阻塞队列使用了锁机制来保证同步。

二、阻塞队列应用场景及使用

1. 生产者-消费者模型

       生产者-消费者模型是一种并发编程中常用的设计模式,用于解决多个线程或进程之间共享资源的同步和协调问题。这种模型基于两种角色:生产者和消费者。

       生产者:负责生成一些数据或资源,并将其放入共享的缓冲区中。
       消费者:从缓冲区中获取资源并处理这些数据或资源。

       生产者消费者模型的目标是确保生产者和消费者之间的协调,避免潜在的竞争资源和数据不一致性问题。

       通俗来讲,生产者-消费者模型就像流水线一样,分工明确,一个负责生产资源,一个负责资源消耗。例如,生产者负责生产饺子皮,而消费者负责消耗饺子皮。

2.生产者-消费者模型的作用

生产者-消费者模型的作用:

  1. 可以让上下游模块,进行更好的解耦合;
  2. 削峰填谷

为了更好的理解生产者-消费者模型的作用,我们先来了解以下的几个名词的含义:
       在软件开发中,“耦合”(Coupling)指的是两个或多个模块、类、组件或系统之间的相互依赖程度。耦合度描述了一个单元对其他单元的了解程度和依赖程度,也可以理解为模块之间交互的紧密程度。“内聚”(Cohesion)是指一个模块、类、组件或系统内部各个元素(函数、方法、属性等)之间相关性和功能关联的程度。内聚度描述了一个单元内部元素之间联系的紧密程度,即这些元素是否在逻辑上紧密相关,是否共同支持同一个功能或目标。

       通俗来讲:"耦合"指的是两个或多个代码块之间关联关系,关联性越强,耦合度越高。"内聚"指的是实现同一个业务的代码或者实现同一功能的代码是否进行统一的管理,如果相关的务逻辑的代码没有放到一起,进行统一的管理,随便乱放,那么说明代码是低内聚的;反之,如果相关的业务逻辑的代码放到一起,进行统一的管理,那么就说明代码是高内聚的。

       作为程序员,我们希望实现相关业务逻辑的代码,是高内聚低耦合的。 在低耦合情况下,不同单元之间的依赖程度较低,它们的变化不会显著影响彼此。低耦合的模块可以独立地进行修改、扩展和维护。模块之间的通信通过抽象接口或中间层来实现,而不需要详细了解对方的内部实现细节。在高内聚情况下,一个模块或组件内部的元素在逻辑上非常紧密相关,它们共同协作以支持同一个主要功能或目标。高内聚的模块通常是专注且自包含的,功能之间的交互和依赖相对较少。

3.应用场景

为了更方便的理解,我们来看一下以下的场景:

       此时 A 服务器和 B 服务器,A 服务器给 B 服务器发送请求,B 服务器给 A 服务器返回响应。我们知道,一个主机或者服务器的资源(内存 和 CPU)是有限的,因此在处理并发事务的能力也是有限的。假设在同一时刻,A 服务器 给 B 服务器发送了大量的请求,超出了 B 服务器的接收能力,内存资源消耗完了,就会导致 B 服务器崩溃的情况。而 A 服务器和 B 服务器之间又是直接通信的,此时的耦合度就会比较高。如果 服务器 B 挂了,对于 A 服务器会有直接的影响,甚至会导致 服务器 A 也会直接崩溃。如果服务器 A 还连接着一个服务器 C,那么服务器 A 的崩溃可能也会直接导致服务器 C 无法正常运行。而且,如果 服务器 A 和 服务器 B 之间的业务如果发生了改变,就需要对代码进行响应的修改,但是此时 服务器 A 和 服务器 C 之间也有业务上的联系,如果修改了 A 的代码,也需要修改 C 的代码,这种情况就是高耦合度带来的危害。 同时,如果某一个时刻,A 发送的请求剧增,会给 B 和 C 带来巨大的压力,导致 B 和 C 在同一时刻消耗大量的资源,导致业务处理能力下降甚至是崩溃。

在这里插入图片描述

加入生产者-消费者模型:
在这里插入图片描述

       此时,我们在三个服务器之间加入一个阻塞队列服务器,用于接收服务器 A 发送的请求和返回服务器 B 和 服务器 C的响应,阻塞队列同时还负责给服务器 B 和 服务器 C 发送服务器 A 的请求,同时接收 服务器 B 和 服务器 C 的响应。此时服务器 A 负责发送请求,是生产者,而服务器 B 和 服务器 C 负责处理请求,是消费者。虽然阻塞队列服务器的资源也是有限的,但是即使是因为接收了太多的请求和响应,导致阻塞队列服务器崩溃也不会导致 A、B、C 三个服务器崩溃,虽然阻塞队列服务器崩溃后,A、B、C 三个服务器不能完成当前业务的执行,但是还可以执行其他的业务逻辑。这样就降低了A、B、C 三个服务器之间的耦合关系。同时,加入阻塞队列后,即使在某一时刻 A 发送的请求剧增,也不会给 B 和 C 带来太大的影响,因为此时 B 和 C 的压力已经转移到了 阻塞队列服务器 这里,而阻塞队列系统资源和承受能力一般是要比普通的服务器要强的,即使是请求量剧增,阻塞队列服务器处理的请求的能力和速度依然基本保持不变,还是会按照相同的速度给 B 和 C 发送请求, B 和 C 只需要按照原来的速度处理请求即可,这样就达到了削峰的作用。当 A 的请求量突然下降之后, B 和 C 也不会突然一下子就会空闲出很多的进程,导致系统资源浪费,因为 阻塞队列服务器 中还有之前请求量剧增时的请求没有处理,因此可能保证一段时间内 B 和 C 的进程可以一直执行下去,不会导致系统资源的浪费,这就实现了填谷的作用

4.生产者-消费者的模拟实现

  • 阻塞队列创建的两种方式:

1.数组实现
           BlockingQueue<> blockingQueue = new ArrayBlockingQueue(5);
2.链表实现
           lockingQueue<> blockingQueue = new LinkedBlockingQueue();

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

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

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

运行结果:
在这里插入图片描述
       上述的代码,是在大部分情况下出现的场景,此时,生产者生产一个资源,消费者就消耗一个资源,不会出现某一时刻,生产资源剧增的情况而消费者处理不过来的情况(类似于上述举例的 服务器 A 和 服务器 B 在大部分情况下应用场景)。

       我们再来看看,某一时刻下,生产者生产的资源剧增的情况下,消费者如何通过阻塞队列进行处理的:

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


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

        Thread producer = new Thread(() -> {
            int value = 0;
            while (true) {
                try {
                    System.out.println("生产元素:" + value);
                    blockingQueue.put(value);
                    value++;

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        producer.start();
    }
}

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

       此时会发现即使是生产者在某一时刻生产的资源剧增(类似于 服务器 A 某一时刻请求量剧增的情况),也不会影响到消费者的执行(类似于 服务器 B 处理请求的情况),因为阻塞队列帮消费者承担了这部分压力,使得程序能够正常的运行下去。


5.阻塞队列的模拟实现

class MyBlockingQueue {
    int[] items = new int[1000];
    volatile public int head = 0;
    volatile public int tail = 0;
    volatile public int size = 0;

    //入队列
    synchronized public void put(int elem) throws InterruptedException {
        while (size == items.length) {
            // 如果队列为满就阻塞,知道队列不为满时唤醒
            this.wait();
        }
        items[tail] = elem;
        tail++;
        //如果 tail 到了队尾,就循环
        if (tail == items.length) {
            tail = 0;
        }
        size++;
        notify();
    }

    synchronized public Integer take() throws InterruptedException {
        while (size == 0) {
            //如果队列为空就阻塞等待,知道队列不为空被唤醒
            this.wait();
        }
        int value = items[head];
        head++;
        //如果 head 到了队尾,就循环
        if (head == items.length) {
            head = 0;
        }
        size--;
        notify();
        return value;
    }


}

public class MyBlockingQueueDemo {
    public static void main(String[] args) {
        MyBlockingQueue blockingQueue = new MyBlockingQueue();

        //消费者
        Thread t1 = new Thread(() -> {
            while (true) {
                try {
                    int value = blockingQueue.take();
                    Thread.sleep(1000);
                    System.out.println("消费者:" + value);

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        //生产者
        Thread t2 = new Thread(() -> {
            int value = 0;
            while (true) {
                try {

                    System.out.println("生产者:" + value);
                    blockingQueue.put(value);
                    Thread.sleep(1000);
                    value++;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t1.start();
        t2.start();
    }
}

运行结果:

类似于服务器 A 和 服务器 B 在大部分情况下的场景,A 发送一个请求,B 就处理一个请求的情况。
                                   
在这里插入图片描述
              
削峰填谷:
              

class MyBlockingQueue {
    int[] items = new int[1000];
    volatile public int head = 0;
    volatile public int tail = 0;
    volatile public int size = 0;

    //入队列
    synchronized public void put(int elem) throws InterruptedException {
        while (size == items.length) {
            // 如果队列为满就阻塞,知道队列不为满时唤醒
            this.wait();
        }
        items[tail] = elem;
        tail++;
        //如果 tail 到了队尾,就循环
        if (tail == items.length) {
            tail = 0;
        }
        size++;
        notify();
    }

    synchronized public Integer take() throws InterruptedException {
        while (size == 0) {
            //如果队列为空就阻塞等待,知道队列不为空被唤醒
            this.wait();
        }
        int value = items[head];
        head++;
        //如果 head 到了队尾,就循环
        if (head == items.length) {
            head = 0;
        }
        size--;
        notify();
        return value;
    }


}

public class MyBlockingQueueDemo {
    public static void main(String[] args) {
        MyBlockingQueue blockingQueue = new MyBlockingQueue();

        //消费者
        Thread t1 = new Thread(() -> {
            while (true) {
                try {
                    int value = blockingQueue.take();
                    Thread.sleep(1000);
                    System.out.println("消费者:" + value);

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        //生产者
        Thread t2 = new Thread(() -> {
            int value = 0;
            while (true) {
                try {

                    System.out.println("生产者:" + value);
                    blockingQueue.put(value);
                    value++;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t1.start();
        t2.start();
    }
}

运行结果:

       类似于某一时刻下,A 产生大量的请求,通过阻塞队列服务器的缓冲,可以使 B 正常运行的情况。也就是削峰填谷的效果。
              
       
在这里插入图片描述


总结

       生产者-消费者模型解决的是多线程或多进程之间数据共享和同步的问题,而阻塞队列作为一种数据结构,则为实现生产者-消费者模型提供了方便的工具。通过使用阻塞队列,可以减少开发人员处理并发问题的复杂性,同时提高代码的可维护性和可读性。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值