当前位置: 首页> 健康> 知识 > 互联网推广员是做什么_网站备案申请_关键词林俊杰百度云_网页自动点击软件

互联网推广员是做什么_网站备案申请_关键词林俊杰百度云_网页自动点击软件

时间:2025/8/10 21:51:16来源:https://blog.csdn.net/HKJ_numb1/article/details/143050874 浏览次数:0次
互联网推广员是做什么_网站备案申请_关键词林俊杰百度云_网页自动点击软件

目录

·前言

一、什么是阻塞队列

二、生产者消费者模型

1.引例

2.作用

(1)解耦合

(2)削峰填谷

三、实现阻塞队列

1.标准库中的阻塞队列

(1)介绍

(2)使用

2.实现阻塞队列

(1)普通队列实现

(2)改进成阻塞队列

·结尾


·前言

        本篇文章会利用多线程来实现一个简单的阻塞队列,我在下面文章中会先介绍什么是阻塞队列,什么是“生产者消费者模型”,基于阻塞队列实现的“生产者消费者模型”有什么作用,最后利用标准库中的阻塞队列来实现“生产者消费者模型”以及使用我们自己实现的阻塞队列来实现“生产者消费者模型”来帮助大家更好的理解阻塞队列,下面就进行本篇文章的内容吧。

一、什么是阻塞队列

        大家都知道,队列是一种“先进先出”的数据结构,阻塞队列就是基于普通队列做出的扩展,所以也遵循“先进先出”这样的原则,那么阻塞队列和普通队列有什么区别呢?主要区别有以下两点:

  1. 阻塞队列是一种线程安全的数据结构;
  2. 阻塞队列具有阻塞特性。

        阻塞队列具有以下的阻塞特性:

  1.  如果针对一个已经满了的队列进行入队列操作,此时入队列操作就会阻塞,一直阻塞到队列不满(其他线程对这个队列进行出队列操作)之后;
  2.  如果针对一个已经空了的队列进行出队列操作,此时出队列操作就会阻塞,一直阻塞到队列不空(其他线程对这个队列进行入队列操作)之后。

        正是因为阻塞队列有上面这些特性,所以使阻塞队列在实际开发中有很大的用处,阻塞队列的一个典型应用场景就是“生产者消费者模型”,这是一种非常典型的开发模型,下面就来介绍一下什么是“生产者消费者模型”。 

二、生产者消费者模型

1.引例

        为了可以更直观的介绍“生产者消费者模型”的作用,下面我举一个生活中的例子,比如过年包饺子的场景,我们都知道,包饺子主要有三个流程:1.和面;2.擀饺子皮;3.包饺子。上面这三个流程,和面一般是一个人负责,这里没办法多人一起来,也就不会使用多线程,但是擀饺子皮和包饺子就可以多人来完成,也就相当于使用多线程,这样的话包饺子就可以利用下面这样的模型,如下图所示:

        如上图中这三个滑稽老铁,就开始共同完成 擀饺子皮和包饺子 的流程,这里每个滑稽都是先擀一个饺子皮,然后包一个饺子,再擀一个饺子皮,再包一个饺子,很显然这种包饺子方式的效率一定是比一个人包快(多线程执行效率比单线程高),但是在这种模型下,我们还要考虑一个因素,那就是在一般家庭,擀面杖是只有一个的,此时这三个滑稽老铁就要竞争这个擀面杖,1号滑稽老铁拿到擀面杖,2号和3号滑稽老铁就要阻塞等待(三个线程竞争同一把锁,一个线程获得锁,另外两个线程就需要阻塞等待) ,为了进一步提高效率,解决这种因为竞争擀面杖(锁)而出现的开销,我们就可以使用分工协作的方式,如下图所示:

        此时,擀面杖就被专门负责擀饺子皮的 1号滑稽独占了,就不会出现有人和他竞争(不会出现锁竞争), 上面的分工协作就构成了“生产者消费者模型”,擀饺子皮的 1号滑稽就是生产者,每擀完一个饺子皮,饺子皮数量就 +1,另外的 2号滑稽和 3号滑稽两个包饺子的,就是消费者,每包完一个饺子,饺子皮数量就 -1。

        其中,桌子就起到了传递饺子皮的作用,这里桌子的角色就相当于“阻塞队列”,假设,1号滑稽擀饺子皮的速度非常快,2号和 3号滑稽包的很慢,就会导致桌子上的饺子皮越来越多,一直到满,此时 1号滑稽就要停下来等一等,等 2号和 3号滑稽消费一波饺子皮,桌子上饺子皮不满的时候再继续擀,反之,如果 1号滑稽擀饺子皮的速度很慢, 2号和 3号滑稽包饺子的速度非常快,就会导致桌子上的饺子皮越来越少,一直到桌子上没有饺子皮了,此时 2号和 3号滑稽就要停下来等一等,等 1号滑稽生产出一波饺子皮,桌子上的饺子皮不为空的时候继续包。

2.作用

(1)解耦合

        在我们编写代码的时候,都会追求写一个“低耦合高内聚”的代码,引入“生产者消费者模型”,就可以更好的做到“解耦合”,也就是将代码的耦合程度从高降低,在开发中涉及到的“分布式系统”,服务器整个的功能不是由一个服务器全部完成,而是每个服务器都负责一部分功能,通过服务器之间的网络通信,最终完成整个功能,如下图所示的模型:

         在上图中的模型中,A 和 B 、A 和 C 之间的耦合性就比较强,这是因为,A需要调用 B、C服务器,此时,A服务器 的代码中就需要涉及到一些和 B服务器 相关的操作,同时 B服务器 的代码中也会涉及到一些和 A服务器 的相关操作,同理,A服务器 和 C服务器 也会存在这样的关系,那么此时,如果 B服务器 或 C服务器 出现了问题,对于 A服务器 来说就会有很大的影响。

        想要降低上面模型中的影响,也就是降低每个模块间代码的耦合关系,就可以引入“生产者消费者模型”,如下图所示:

        引入“生产者消费者模型”后,此时,服务器A 与 服务器B、C,之间都不是直接交互了,而是通过阻塞队列在中间“传话”,A中代码只需要和 队列 交互,不知道 B 和 C 的存在,B、C 中的代码中也只需要和 队列 交互,不知道 A 的存在,这时就算 B、C 出现问题,对 A 的影响也就微乎其微了,假设这里以后要再新增一个 D 服务器,A 的代码也几乎不用变化。 

(2)削峰填谷

        生活中我们熟知的水库,就有着削峰填谷的作用,在降雨量骤增达到峰值的时候,水库可以蓄水,并以正常的速度向外排水,防止出现“洪涝灾害”,当降雨量骤减到低谷,水库又可以多放出些水来填补低谷,引入“生产者消费者模型”就可以起到削峰填谷的作用,如下图中的没有引入“生产者消费者模型”场景所示:

         此时,当客户端请求突然增多,A 的请求数量增多压力就会变大,但是 入口服务器做的工作一般比较简单,每个请求的消耗资源比较少,会又影响,但是不会有严重的问题,不过对于 B、C 服务器,就没有那么幸运了,服务器B 需要在数据库找到对应用户的信息,服务器C 需要在数据库中找到对应文章的信息,还有一下规则需要匹配,过滤……,这里B、C 的工作更复杂,每个请求消耗的资源更多,所以随着请求的增多,B、C 的压力越来越大,很可能 B、C服务器 就会出现一些严重的问题,导致整个系统瘫痪。

        为了缓解上述的问题,我们就可以引入“生产者消费者模型”,如下图所示:

        由于阻塞队列中没有什么复杂的执行逻辑,只是存储数据,所以抗压的能力会比较强,这样即使突然请求量出现峰值,也是由阻塞队列来承担峰值的请求,这里的B、C 仍然按照之前的速度来取请求和返回响应,引入“生产者消费者模型”就可以有效的防止 B、C服务器 因为请求量突然达到峰值而出现严重的问题。 

三、实现阻塞队列

1.标准库中的阻塞队列

(1)介绍

        在 Java 标准库中就提供了现成阻塞队列这样的数据结构:BlockingQueue ,这里 BlockingQueue 是一个接口,实现这个接口的类也有很多,如下图所示:

        下面就以 LinkedBlockingQueue 为实现类来使用一下标准库中的阻塞队列。如下图所示:         图中的 put 方法是带有阻塞的入队列,take 方法是带有阻塞的出队列,在 BlockingQueue 中也提供了 offer、poll、peek 等方法,但是这些方法都不带有阻塞的特性。

        下面测试一下 put 与 take 方法吧,代码及运行结果如下所示:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;public class ThreadDemo4 {public static void main(String[] args) throws InterruptedException {BlockingQueue<String> blockingQueue = new LinkedBlockingQueue();blockingQueue.put("aaaa");String elem = blockingQueue.take();System.out.println(elem);}
}

  

(2)使用

        下面就使用标准库提供的阻塞队列来实现一个“生产者消费者模型”,具体代码及运行结果如下所示:

import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;public class ThreadDemo4 {public static void main(String[] args) throws InterruptedException {BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue();Thread customer = new Thread(()->{while (true) {try {// 获取消费的元素并输出到控制台上Integer elem = blockingQueue.take();System.out.println("消费元素:->" + elem);} catch (InterruptedException e) {throw new RuntimeException(e);}}},"消费者");Thread producer = new Thread(()->{// 创建 Random 对象,用于生成随机数Random random = new Random();while (true) {// 获取生产元素的值Integer elem = random.nextInt(1000);System.out.println("生产元素:->" + elem);try {// 将生产出的元素加入到阻塞队列blockingQueue.put(elem);Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});// 启动消费者线程与生产者线程customer.start();producer.start();}
}

        上面就是利用标准库中的阻塞队列实现的"生产者消费者模型"了,下面我们来自己实现一个简单阻塞队列,并基于自己实现的阻塞队列也实现一个“生产者消费者模型”。

2.实现阻塞队列

        实现阻塞队列,我们可以从浅到深的来实现,先实现一个普通队列,再在普通队列的基础上,添加上线程安全,再增加阻塞功能,那么就来普通队列的实现吧。

(1)普通队列实现

        我们这里要实现的普通队列是一个基于数组来实现的环形队列,如下图所示:

        环形队列是由数组抽象出来的结构,这里我们使用一个 size 变量来记录队列中元素个数,这样当 head 与 tail 重合的时候就可以由 size 的值来判断当前队列是空的还是满的(head 与 tail 重合,size 值不是 0,就代表队列满,size = 0,代表队列为空),具体的实现细节,与详细的介绍如下列代码及其中注释所示:

class MyBlockingQueue {// 创建 String 数组模拟队列private String[] elems = null;// head 记录队列的头private int head = 0;// tail 记录队列的尾private int tail = 0;// size 记录当前队列中元素个数private int size = 0;// 构造方法,创建出一个长度为 len 的队列public MyBlockingQueue(int len) {elems = new String[len];}// 向队列中添加元素 elempublic void put(String elem) {// 判断当前队列是否为满if (size >= elems.length) {return;}// 将元素入队列elems[tail] = elem;// tail 向后移动tail++;// 判断当前 tail 所在位置是否超过队列长度if (tail >= elems.length) {// 下标超过队列长度就回到 0 号下标位置tail = 0;}// 入队列成功,队列元素个数 +1size++;}// 从队列中取出元素,并返回public String take() {// 判断当前队列是否为空if (size == 0) {return null;}// 用 ret 接收方法返回值String ret = elems[head];// head 向后移动head++;// 判断当前 head 所在位置是否超过队列长度if (head >= elems.length) {// 下标超过队列长度就回到 0 号下标位置head = 0;}// 出队列成功,队列中元素个数 -1size--;// 返回取出的元素值return ret;}
}

(2)改进成阻塞队列

        上述普通队列在多线程的运行环境下会涉及到线程安全的问题,这里主要问题是在 put 和 take 方法上,这两个方法都涉及到对同一个队列进行操作,在多线程对同一个对象进行修改操作时就容易出现线程安全问题,上面代码的问题如下图情况所示:

        如上图所示,在线程 t1 执行完将元素入队列的操作后, t1 线程被调度走,到 t2 线程执行,t2 线程,又在之前 tail(此时 tail 的值还是之前进入方法时 t2 线程拿到的 tail,没有改变) 的位置上进行入队列操作,就会覆盖之前 t1 入队列的值,就会出现线程安全问题。

        那么解决上述问题的方法也很简单,那就是进行加锁,对整个 put 方法进行加锁(可以直接加到方法上,也可以利用锁对象来对方法中所有代码进行加锁),我们选择使用锁对象进行加锁,这种方式比较容易理解。同样解决出队列的线程安全问题也是对整个 take 方法进行加锁。

        加完锁后,线程安全是可以保证了,但是还没有实现阻塞的特性,此时的队列,如果满进行入队列操作就会直接返回,而我们需要让他在执行入队列时,如果队列满就阻塞等待这样的效果,这时就需要搭配我们的 wait 与 notify 方法了,具体使用方式,如下图所示:

        将 wait 与 notify 方法加上后,此时代码还有没有什么问题呢?其实还是有的,比如这上面的if 语句,上图中使用 if 语句是否合理?其实这里可能还会出现问题,如下图所示的场景: 

        此时队列为满,t2 与 t3 线程执行到 put 方法时都进行了 wait,此时线程 t1 执行了 take 方法取出了一个元素,同时使用 notify 方法随机唤醒了一个正在 wait 方法,这里假设唤醒的是 t2 线程,这时,t2 线程就会继续执行 put 方法,添加完元素后,队列又满了,但是此时,t2 线程调用的 notify 方法唤醒了线程 t3 的 wait,t3 线程就继续执行 put 方法,这时候就出现问题了,此时队列为满按我们的想法 t3 线程需要继续阻塞等待,可是这里 t3 线程由于 t2 线程的 notify 方法被意外唤醒了,所以也就出现了问题。

        那么出现上述问题的原因是什么, 一是 notify 方法是随机唤醒的,二是因为 if 语句是“一锤子买卖”,只进行了一次判断,这就导致 put 方法在唤醒时没有重新进行判断当前队列是否为满就继续执行下面代码了,要想解决上面的问题也很简单,将 if 换成 while 即可,这样就意味着 wait 唤醒之后要再进行一次判定条件,如果再次判定,发现队列还是满的就会继续阻塞等待,通过查看源码也可以发现,Java 标准库中也推荐我们在使用 wait 方法时要搭配 while,如下图所示:

        那么解决完上述问题之后,我们就可以基于普通队列进行修改,实现出阻塞队列,从而我们就可以利用我们实现的阻塞队列实现“生产者消费者模型”了,阻塞队列与实现“生产者消费者模型”具体代码及运行结果如下所示:

class MyBlockingQueue {// 创建 String 数组模拟队列private String[] elems = null;// head 记录队列的头private int head = 0;// tail 记录队列的尾private int tail = 0;// size 记录当前队列中元素个数private int size = 0;// locker 作为锁对象,进行加锁操作时使用private Object locker = new Object();// 构造方法,创建出一个长度为 len 的队列public MyBlockingQueue(int len) {elems = new String[len];}// 向队列中添加元素 elempublic void put(String elem) throws InterruptedException {// 锁加这里与加到方法上的本质是一样的,加到方法上是给 this 加锁,此处是给 locker 加锁synchronized (locker) {while (size >= elems.length) {// 队列满了,需要阻塞等待其他线程对这个队列进行出队列操作唤醒此处 waitlocker.wait();}// 新的元素放到 tail 指向的位置elems[tail] = elem;tail++;if (tail >= elems.length) {// 下标超过队列长度就回到 0 号下标位置tail = 0;}// 入队列成功,队列元素个数 +1size++;// 入队列成功后唤醒locker.notify();}}// 从队列中取出元素,并返回public String take() throws InterruptedException {String ret = null;synchronized (locker) {while (size == 0) {// 队列为空,需要阻塞等待其他线程对这个队列进行入队列操作,唤醒此处的 waitlocker.wait();}// 取出 head 位置的元素ret = elems[head];head++;if (head >= elems.length) {// 下标超过队列长度就回到 0 号下标位置head = 0;}// 出队列成功,队列中元素个数 -1size--;// 元素出队列成功后唤醒locker.notify();}// 返回取出的元素值return ret;}
}public class ThreadDemo5 {public static void main(String[] args) throws InterruptedException {MyBlockingQueue myBlockingQueue = new MyBlockingQueue(100);Thread customer = new Thread(()->{while (true) {try {String elem = myBlockingQueue.take();System.out.println("消费元素:-> " + elem);Thread.sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}}},"消费者");Thread producer = new Thread(()->{int n = 0;while (true) {try {myBlockingQueue.put(n + "");System.out.println("生产元素:-> " + n);} catch (InterruptedException e) {throw new RuntimeException(e);}n++;}},"生产者");// 启动生产者与消费者线程customer.start();producer.start();}
}

        由于上述代码中,消费者线程在每次循环时会休眠 500ms,生产者线程每次循环没有休眠,这就导致,在两个线程启动的时候,生产者线程会一下把阻塞队列给生产满,就出现上图中运行的结果了,一开始生产者一下就生产了 100 个元素,然后生产者和消费者开始同步进行生产消费。

·结尾

        文章到此处就要结束了,本篇文章重点介绍了什么是阻塞队列,基于阻塞队列实现的“生产者消费者模型”是什么,作用有哪些,还介绍了 Java 标准库中阻塞队列的用法,并且自己实现了一个阻塞队列,其中基于循环队列修改成阻塞队列的每一步遇见的问题还是需要好好的理解,本篇文章介绍的阻塞队列其实也可以称为“消息队列”也就是 mq(Message Queue),这是一个非常常用的一种中间件,希望看完本篇文章能让你对阻塞队列以及多线程编程有一个更深的理解,如果感觉文章讲解的还不错,希望能收到你的三连~~~同时,如果你对文章的知识还有所困惑,欢迎在评论区留言进行讨论,我们下一篇文章再见吧~~~

关键字:互联网推广员是做什么_网站备案申请_关键词林俊杰百度云_网页自动点击软件

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

责任编辑: