当前位置: 首页 > article >正文

JavaEE——多线程代码案例2:阻塞队列

阻塞队列的概念:

基于普通队列,做出的扩展,也是先进先出

阻塞队列的特点:

  1. 线程安全的

    标准库中的容器——队列Queue是线程不安全的。
    集合里的大多是线程不安全的。集合里的所谓线程安全的几个,是在关键方法加了锁,相对安全。(并不是加锁就一定安全)
  2. 具有阻塞特性

    (a)如果针对一个已经满了的队列进行入队列,此时入队列的操作就会阻塞,一直阻塞到队列不满(其他线程出队列元素)之后。
    (b)如果针对一个已经空了的队里进行出队列,此时出队列的操作就会阻塞,一直阻塞到队列不空(其他线程入队列元素)之后。
     

阻塞队里的用处

基于阻塞队列,可以实现“生产者消费者模型”——用处很大

【生产者消费者模型】描述的是一种多线程编程的方法 ~ ~(分工协作 来 解决问题,比单线程效率高)(流水线工作快)
在这个模型中,阻塞队列充当传递数据的作用。

生产者消费者模型的意义

生产者消费者模型,在实际开发中有非常重要的意义。

  1. 意义一:解耦合
    引入生产者消费者模型,就可以更好的做到“解耦合” (降低代码的耦合度)

    【分布式系统】
            实际开发中,服务器整个功能 不是由 一个服务器 完成的。而是 每个服务器负责一部分功能,通过服务器之间的网络通信,最终完成整个功能。(如下图:图一)



    如图所示,这种耦合性比较高。引入“生产者消费者模型”,降低上述的耦合 ~ ~(如下图:)


    【代价】
    这样解决分布式系统耦合性的方法,需要加入机器,引入更多的硬件资源!!!
    (1)上述描述的“阻塞队列”,并非简单的数据结构,而是基于这个数据结构实现的服务器程序,并被部署到单独的主机上了。——消息队列(message queue,mq)

    (2)整个系统的结构更复杂了,需要维护的服务器更多了。

    (3)效率降低。引入中间商(阻塞队列),还是有代价的:请求从A发出来到B收到,这个过程中就经历队列的转发。这个过程有是一定的开销的。
  2. 意义二:削峰填谷
    (此处所谓的“峰”、“谷”,都不是长时间持续的,而是短时间出现的——太长时间仍然会挂)

    图一中:

                【外界请求】属于用户行为,不可预知,数量不固定。

                一旦用户请求出突发峰值,突发峰值就会使B和C直接寄了。。。(一些突发事件导致请求骤增的经典场景:双十一)

               (1) “请求多了,服务器会挂”,原因:
                        服务器处理每个请求都需要消耗硬件资源:
                                CPU——处理代码实现
                                内存、硬盘——存储数据
                                网络带宽——传输数据
                                ……
                                (上述任何一种硬件资源达到瓶颈,服务器都会挂!!!直观的现象就是:你给他发请求,他不会返回响应了)
 

                (2)“A比B、C的抗压能力强”,原因:
                        A:入口服务器,A的工作计较简单,每个请求消耗的资源少(并不是不消耗,请求进一步增多,A也会挂,消息队列也是)
                        B、C完成的工作更复杂,每个请求消耗的资源多 ~ ~
                        (B:用户服务器,要从数据库中找到对应的用户信息        C:商品服务器,也许需要从数据库中找到对应的商品,还一些规则需要匹配,过滤……)

                【削峰填谷——消息队列,处理请求骤增的情况,防止服务器挂了】
                        【注意】
                                阻塞队列——数据结构
                                消息队列——基于阻塞队列实现的服务器程序
                                (概念要清楚,这样区分更有针对性)

                        

如图,在入口服务器A和其他两个需要取请求并相应请求的服务器中间加上一个“消息队列”,起到一个缓冲作用
即使请求骤然发生峰值,也是由队列来承担峰值请求,也不会轻易使B、C挂掉,B、仍然是按照之前的速度来取请求 ~ ~

当然,如果请求一直往上增加,A和消息队列肯定也会有顶不住的时候,会挂掉。
对此,大厂的系统,往往会有非常充裕的冗余量 ~ ~(引入更多的硬件资源,避免上述情况)

阻塞队列的用法(比较简单)

Java标准库里提供了现成的阻塞队列数据结构——BlockingQueue(接口,有很多类实现了这个接口,也就有很多种阻塞队列)
 

注意:阻塞队列的类,既有带阻塞的方法,也有普通的队列不带阻塞的方法。(阻塞队列没有带阻塞的获取队首元素的方法,不过它可以使用不带阻塞的)

【put】
入队列,offer也是入队列。
put带有阻塞功能,offer没带阻塞(队列满了会返回boolean类型)

BlockingQueue queue = new ArrayBlockingQueue(100);

queue.put("aaa");

【take】
出队列,……
take带有阻塞功能,……

//

【注意】
阻塞队列没有提供带有阻塞功能的获取队首元素的方法 ~ ~

Java标准库提供的阻塞队列(重点关注用法,很简单)

Java标准库提供了现成的数据结构。

自己实现一个阻塞队列

  1. 先实现普通队列
    ——基于数组来实现(环形队列)

  2. 再加上线程安全

  3. 再加上阻塞功能

public void put(String elem){
    if(size >= elems.length){
        //队列满了
        //后续需要让这个代码能够阻塞
        return;
    }
    //新的元素要放到tail指向的位置上
    elems[tail] = elem;
    tail++;
    
    if(tail >= elems.length){
        tail = 0;
    }
    size++;
}

        【实现普通队列】


      队列满——
      队列空——
      这两个情况的判定,有两种方法:
     (1)浪费一个格子,tail最多走到head的前一个位置
     (2)引入size变量


        评价某个代码好不好的标准,参考的指标:
        1)开发效率(代码是否容易理解)
        2)运行效率 (代码执行的速度快不快)


        写法(1):
        

if(tail >= elems.length){
    tail = 0;
}

开发效率:程序员都能理解if语句

运行效率:if是条件跳转语句,执行速度非常快。并且大多数情况下,不会满足if条件从而不会触发赋值
        
        写法(2):

tail = tail % elems.length;
//如果tail<length,此时求余的值,就是tail原来的值
//如果tail==length,求余的值就是0

开发效率:不是所有程序员都熟悉%操作的特性——这个代码并不好理解

运行效率:%本质上是除法运算指令,除法运算,属于比较低效的指令(CPU更擅长的是加减运算,计算*/的速度要比+-更逊色一筹 ~ ~)

===>综上,推荐写法一(小胜一筹)。不过具体怎么写还是要看个人习惯。虽然有差别,但是差别不大,如果你的程序太卡,大概率不是因为这里的缘故导致的 ~ ~(C++追求性能到极致,会非常扣这些细节,Java更推荐说服老板多加机器 ~ ~)

                ——要知道,很多所谓的“优化”,最后归根结底,是增加机器,增加硬件资源,更为立竿见影。         仅仅是软件层面的优化,当然也会有用,但是投入产出比较低 ~  ~费很大劲,只能优化一点点……

【加上线程安全】——引入锁,解决线程安全问题 ~ ~(出、入队列需要使用同一把锁,否则一个线程出队列,一个线程入队列,不考虑阻塞的情况就有线程安全问题了——没有锁竞争)

引入锁需要考虑的几个问题:

1)如何加锁/锁放到哪里合适?

2)加了锁之后,是否还有问题?

——这两项,都是多线程的难点。需要仔细考虑。确保所有执行顺序下,程序的结果都是对的!!

如上图所示的加锁方式 和 执行顺序下,如果put的是最后一个元素,会多加一个元素(上图只考虑了“都是“写操作”的部分进行加锁了”)

因此,需要将这里的操作打包。也就是如下代码所示:

public void put(String elem){
    synchronized(locker){
        if(size >= elems.length){
            //队列满了
            //后续需要让这个代码能够阻塞。
            return;
        }

        //新的元素要放到 tail 指向的位置上
        elems[tail] = elem;
        tail++;
        if(tail >= elems.length){
            tail = 0;
        }
        size++;
    }
    
}

——这里可以思考一下,如过“上边”“if 判断条件”的代码,会因结果不同,影响到“下边”代码的执行,并且,下边代码的执行结果,会改变上边条件判断的结果,那么,就要考虑 加锁解决可能存在的线程安全问题

【阻塞】

大致原理就是:
出队列操作,如果队列空,则阻塞,等待有入队列成功的操作之后notify唤醒;
入队列操作,如果队里满,则阻塞,等待有出队列成功的操作之后notify唤醒;
——可行原因 及 要求1)队列不可能 既空又满,所以一个一定可以在另一个成功之后被唤醒。
                                   2)根据以上notify唤醒wait的原理分析,入队列和出队列需用同一把锁(对同一个锁对象加锁)

不过,还可能出现其他问题:

这是分析中的理想情况——可以正常执行,且没有问题:

但是,还可能出现下面这种情况:一个notify“连锁唤醒”(我想的描述词 ~ ~)
两个线程都入队列阻塞了:

        线程1入队列阻塞后,线程2出队列操作成功一次,并执行notify,唤醒了线程1,线程1能够成功put了,

        但是在线程1被唤醒后成功put的这次操作后的notify之前,线程3也执行put操作,但是被阻塞了。(这里的顺序描述不必如此严谨,因为notify一次只能唤醒一个使用同一个锁对象的阻塞线程,这里的主要问题是接连触发多个notify唤醒多个wait)

        本来线程2只take一次,只空出了一个位置供put使用,但是,线程1的notify却唤醒了线程3的wait.
        也就是说,入队列的唤醒操作会唤醒其他等待入队列的操作。(一开始有两个wait)

如何解决——if判断改while判断,唤醒之后再判定一次,相当于多做了一步确认操作。
——问题所在:提前被唤醒
(明明条件还没满足,就唤醒了)

——这里可以思考一下:一个线程 被阻塞 到 被唤醒,需要尤其注意这中间的变化,尤其是是否影响到阻塞前的条件判断的条件(如果有条件判断的话——一般会有,满足什么条件,才会进入阻塞),是否在这个过程中发生了变化——由此也可见,唤醒的线程,是从阻塞的位置继续执行的,而不是从该线程从头开始重新执行。
 

理论上,应该每次唤醒都是满足继续执行的条件的。
但是实际并非如此,既然唤醒信号 代表的含义并不可靠,那么就不能再依赖这个信号进行决定是否继续执行,于是,采用其他方法进行判断

每次被唤醒,都应该确认一下,看看当前是否就应应该要继续执行,还是再等待一会儿。也就是:利用while进行多一次判断

 

如下图所示,Java本身也是推荐我们用while这种操作来解决的:

自己实现一个阻塞队列的完整代码如下:

基于这个阻塞队列,可以实现:生产者消费者模型

        实际开发中,生产者消费者模型(最核心的部分仍然是阻塞队列。使用synchronized和wait/notify达到 线程安全/阻塞),往往是多个生产者,多个消费者。
        这里的生产者和消费者不仅仅是一个线程,也可能是一个独立的服务器程序,甚至是一组服务器程序。


http://www.kler.cn/a/502649.html

相关文章:

  • 贪心算法详细讲解(沉淀中)
  • DolphinScheduler自身容错导致的服务器持续崩溃重大问题的排查与解决
  • 服务器数据恢复—raid5故障导致上层ORACLE无法启动的数据恢复案例
  • Kotlin构造函数
  • Windows图形界面(GUI)-QT-C/C++ - QT控件创建管理初始化
  • 细说STM32F407单片机以DMA方式读写外部SRAM的方法
  • 从 SQL 语句到数据库操作
  • 51单片机——DS18B20温度传感器
  • ue5使用蓝图接口记录
  • 【Docker系列】容器内目录显示异常的解决之道
  • 【JVM-3】深入理解JVM堆内存:结构、管理与优化
  • STM32之LWIP网络通讯设计-上(十四)
  • 如何稳定使用 O1 / O1 Pro,让“降智”现象不再困扰?
  • Swagger生成Api文档的增强解决方案--knife4j
  • http和https有哪些不同
  • 【Ubuntu与Linux操作系统:一、Ubuntu安装与基本使用】
  • 45. 跳跃游戏2
  • 使用 Docker 部署 Java 项目(通俗易懂)
  • Java的Stream流和Option类
  • 深入解析 Python 中的函数参数类型详解
  • ​Vue虚拟DOM:如何提高前端开发效率
  • 44_Lua迭代器
  • 【IPy模块01】Python运维模块之IP 地址、网段的基本处理
  • Qt中.pro文件中可以填加的宏和其他的信息
  • 【Linux笔记】Day2
  • Mysql--基础篇--多表查询(JOIN,笛卡尔积)