【多线程初阶四】单例模式阻塞队列

news2024/12/26 21:08:59

目录

🌟一、单例模式              

🌈1、饿汉模式

🌈2、懒汉模式(重点!)

 🌟二、工厂模式  

🌟三、阻塞式队列

🌈1、阻塞队列是什么?

🌈2、阻塞队列:生产者-消费者模型

🌈3、消息队列的作用

🌈4、演示JDK中提供的阻塞队列

🌈5、自己实现阻塞队列(循环队列-数组实现)

🌈6、模拟实现生产者与消费者模型


🌟一、单例模式              

         单例是一种设计模式。这里面有两个关键字,一个是单例,一个是设计模式。什么是设计模式呢?设计模式指的就是业内的大神们根据以往的程序设计经验总结出来的一套方法。是类似于棋谱一样的东西。而单例是什么?单例指的是在全局范围内只有一个实例对象。比如之前数据库的JDBC中就只有一个DataSource。定义数据库的用户名,密码,连接串之后就可以通过DataSource的实例对象获取数据库的连接。参考链接

        单例模式的实现方式主要有两种:饿汉模式和懒汉模式。这两个是手写代码都得写出来的重要程度~🤣

🌈1、饿汉模式

        因为单例在全局范围内只有一个实例对象,因此我们需要清楚在Java中哪些对象是全局唯一的。在JavaSE部分我们学过,用static修饰的变量是类的成员变量,所有的实例对象,访问的都是同一个成员变量。因此通过类对象与static配合使用可以实现单例。

         既然是单例,上述用new的方式是有歧义的,new用来表示产生新的对象。因此我们对上述的代码进行改造,将构造方法进行私有化,直接通过静态方法来调用。

        上面这种当类一加载就完成初始化的方式称为饿汉模式。书写简单,不容易出错。 存在的问题:存在很多的资源浪费。

🌈2、懒汉模式(重点!)

        在类加载的时候不创建实例,只有在第一次使用的时候才创建实例,避免程序启动的时候浪费过多的系统资源。

(1)单线程环境测试

 在单线程环境下是没有问题的,我们接着看一下在多线程环境中。

(2)多线程环境测试

  分析出出现线层不安全的原因:

 解决办法:加锁——>确定加锁范围 

 (1)对整个方法加锁

  (2)对代码块加锁:但是要加在if判断条件外。

 注意:如果在If条件条件内加锁,是不正确的。

 比如t1线程现在判断为空,t1执行if中的代码,t2也为空,执行If中的代码。也就是说对象还是创建了两次,所以不正确。

(3)对加锁范围优化

        不过上述解决办法还是存在一定的问题:

         synchronized代码块在整个程序运行过程中,只要去创建实例调用getInstance()方法,那么久都要参与锁竞争,非常浪费系统资源。实际上我们只需要执行一次就够了,在进行锁竞争之前,判断一下是否已经初始化创建了对象,如果没有再去执行。因此在外面再加入一层if条件判断,这种用两层If条件判断的方式叫做“双重检查锁”。

❓问题:为什么会浪费系统资源?(简单了解)

        我们需要知道,用户态和内核态两个概念。用户态是指在Java层面,在JVM中执行的代码。内核态是指执行的是CPU指令,也就是说加入synchronized参与锁竞争之后就从应用层面进入了系统层面。内核态要进行的所有的操作,不单是我们自己写好的程序。

        比如银行柜员和用户,银行柜员就相当于内核态,它不仅需要处理顾客的需求,帮忙复印文件,而且还要处理领导的事情,同事的事情等;顾客相当于用户态,只需要复印自己的文件就行。所以在交给内核态银行柜员处理业务之前,尽可能的将自己的事情做好,这样内核他就可以少做一些事情,从而提升效率。

 (4)通过上述synchronized加锁解决了原子性,内存可见性。那么有序性如何保证呢?——>对共享变量加入volatile关键字。

❓ 问题:为什么要保证有序性?

        

         在初始化的过程中,并不是一条指令。整个初始化过程中包括LOAD,ASSGIN,STORE。1在内存中开辟一片空间——>2初始化内存的属性——>3将内存中的地址赋值给instance变量。而程序执行的过程中也是按照123这个顺序来的。但是我们知道,编译器可能会对指令重排序,对代码的执行顺序进行优化。我们可以看出1和3是一个强相关的执行过程,都与内存有关系,2是单独的一条指令,因此编译器可能会对通过指令重排序从而改变代码的执行顺序变为132。如果执行顺序变成了132这种,那么在在指定到第三步的时候,将instance变量存储到内存中了,但是该变量还没有进行初始化(第二步还没有执行),那么其他线程就有可能拿到一个创建了一半的对象。因此说这个对象是一个不安全的对象。所以要用volatile修饰变量,禁止指令重排序。

        

💯 总结

一、单例模式

1、饿汉模式:随着程序加载就完成了初始化。工作中可以使用饿汉模式,书写简单且不容易出错。 

2、饿汉模式存在的问题:资源浪费。由于计算机资源有限,为了节约资源,可以使用懒汉模式加载。

3、懒汉模式:在程序加载的时候完成的初始化。

4、懒汉模式存在的问题:在单线程下正常,在多线程环境下可能会出现线程不安全的现象。

5、懒汉模式线程不安全的解决办法:使用synchronized加锁(在方法中加锁或者if外的代码块加锁)

6、上述加锁存在的问题:由于初始化代码只执行一次,后续的线程在调用getInstance方法的时候,依然会产生锁竞争,频繁的进行用户态与内核态之间的切换,非常浪费计算机资源。

7、解决办法:使用DCL(double check lock)DCL双重锁的方式,双层If判断,外面的If用于非空校验,避免无用的锁竞争。

8、通过synchronized解决了原子性,内存可见性的问题,再使用volatile解决由于有序性问题:用volatile修饰共享变量即可。

9、有序性问题主要是因为指令重排序现象的发生。描述其过程。        


 🌟二、工厂模式  

观察下述代码的现象:

 解决办法:工厂模式就是用于解决构造方法创建对象的不足。


🌟三、阻塞式队列

🌈1、阻塞队列是什么?

        之前我们学习过队列,是一种先进先出的数据结构。阻塞队列也满足队列的特性,主要表现为: 入队元素时,先判断队列是否已经满了,如果满了就阻塞等待,当有空闲空间再插入;

        出队元素时,先判断队列是个已经空了,如果空了就阻塞等待,当队列中有元素时再出队。

🌈2、阻塞队列:生产者-消费者模型

        阻塞队列的一个典型的应用场景就是“生产者消费者模型”,就是通过一个容器来解决生产者和消费者之间的强耦合关系。生产者和消费者之间并不直接通信,而是通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等消费者处理,直接交给阻塞队列;消费者也不需要找生产者,直接从阻塞队列中取数据就行。

        消息队列本身就是一个阻塞队列,在此基础上为放入阻塞队列的消息打上一个标签。打标签可以实现分组的效果。

🌈3、消息队列的作用

(1)解耦

        在设计程序的时候,一般要求“高内聚,低耦合”。高内聚:指的是将功能强相关的代码写在一起,方便维护;低耦合:指的是将功能相同的代码封装成一个方法,使用的时候直接调用即可。

(2)削峰填谷

栗子🌰:在工程环境中的应用:

         当在微博发生热点事件或者是双十一这种场景下,任何一个节点出现问题都会影响整个业务流程。解决办法:可以使用多个A和B服务器解决,但是实际生活中这种流量暴增的时刻是短暂的,时间点过后都会回归平常,因此这种方式会造成很大的资源浪费,不太适用。因此比较好的方法就是使用消息队列来解决。(消息队列是非常吃内存的)。当然,在这种情况下也可以并行的使用多个A服务器,相较于前一种方法会减少资源的消耗。同样的,当调用第三方服务器接口的时候,在峰值的那一瞬间,是很难直接同时处理这么多的消息的。因此如果调用次数达到上限,那就阻塞一会。也就是说,服务器同一时刻可能会收到大量的支付请求. 如果直接处理这些支付请求,服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程)。这个时候就可以把这些请求都放到一个阻塞队列中, 然后再由消费者线程慢慢的来处理每个支付请求。这样做可以有效进行 "削峰",防止服务器被突然到来的一波请求直接冲垮。   

(3)异步

同步:请求方必须死等对方的回应;

异步:发出请求之后,自己去干别的事情,在有响应的时候会接收到通知从而处理响应。

🌈4、演示JDK中提供的阻塞队列

需要知道:

        (1)创建阻塞队列。BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue
 

        (2)往阻塞队列中添加元素

                queue.put();

        (3)从阻塞队列中取元素

                queue.take();  之前的普通的队列取元素是 poll();

public static void main(String[] args) throws InterruptedException {
        //基于链表实现的BlockingQueue:还有其他的LinkedBlockingQueue, ArrayBlockingQueue
        //1、创建的时候指定队列的容量:当队列满的时候再插入元素就会阻塞
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(3);
        //2、往队列中写入元素
        queue.put(1);
        queue.put(2);
        queue.put(3);
        System.out.println("插入了3个元素");
        System.out.println(queue);
        //3、队列已满,此时会阻塞,不会打印下面这句话
//        queue.put(4);
//        System.out.println("插入了4个元素");
        //4、阻塞队列中取出元素
        System.out.println("获取的第一个元素是"+queue.take());
        System.out.println("获取的第二个元素是"+queue.take());
        System.out.println("获取的第三个元素是"+queue.take());
        //此时队列已经空了,进入阻塞状态
        System.out.println("获取的第四个元素是"+queue.take());
    }

控制台输出:

🌈5、自己实现阻塞队列(循环队列-数组实现)

 //之前队列底层实现用到的是链表和循环数组。现在阻塞队列就是在其基础上增加了阻塞等待的操作。
    //此处以数组实现为例:定义初始化变量
    //定义一个保存元素的数组
    private volatile int[] elementData = new int[3];
    //定义头部索引
    private volatile int head;
    //定义尾部索引
    private volatile int tail;
    //定义有效元素的个数
    private volatile int size;
    /**
     * 插入一个元素
     * @param val
     */
    public void put(int val) throws InterruptedException {
        //------在普通队列基础上加入等待和唤醒的操作,这两个操作与synchronized息息相关。
        //------一般new出来的对象,锁对象都是this(指大部分情况)
        synchronized(this){
            //1、判断元素是否已满:满了就阻塞等待
            while(size >= elementData.length){
                this.wait();
            }
            //2、队尾插入元素
            elementData[tail] = val;
            //3、更新tail索引:循环队列(之前队列的实现用得是取模运算,不是简单的++)
            tail++;
            if(tail >= elementData.length){
                tail=0;
            }
            //4、更新元素个数
            size++;
            //5、唤醒
            notifyAll();
        }
    }

    /**
     * 取出元素
     * @return
     */
    public int take() throws InterruptedException {
        //根据共享范围加锁,锁对象是this即可
        synchronized (this) {
            //1、判断队列是否为空:为空就阻塞
            while(size <= 0) {
                this.wait();
            }
            //2、从队首出队
            int val = elementData[head];
            //3、更新head下标
            head++;
            if (head >= elementData.length) {
                head = 0;
            }
            //4、修改有效元素的个数
            size--;
            //5、唤醒
            notifyAll();
            //6、返回队首元素
            return val;
        }
    }

测试类:

 public static void main(String[] args) throws InterruptedException {
        a02_MyBlockQueue deque = new a02_MyBlockQueue();
        deque.put(1);
        deque.put(2);
        deque.put(3);
        //Ctrl D复制当前行 Ctrl Y删除当前行
        System.out.println("已经插入三个元素");
        //阻塞等待
//        deque.put(4);
//        System.out.println(deque);
//        System.out.println("已经插入四个元素");

        deque.take();
        deque.take();
        deque.take();
        System.out.println("已经出队3个元素");
        //阻塞等待
        deque.take();
        System.out.println("已经出队4个队列");
    }

      🚨注意

        (1) 虚假唤醒:

        在第一次满足wait() 时,线程进入阻塞等待。但是在这个期间可能会发生很多事情,因此当被唤醒之后,应该再次检查线程的状态。使用while,而不是if。

        

 (2)对于所有的共享变量都要加volatile。

  (3)使用synchronized进行加锁控制。

( 4)上述是通过循环队列的方式来实现的。

🌈6、模拟实现生产者与消费者模型

分别用一个线程模拟生产者和消费者。

//模拟目标:将生产者生产的商品先放入消息队列中存储,等过10秒之后,消费者再开始消费
    //1、定义一个阻塞队列
    private static BlockingQueue queue = new LinkedBlockingQueue(100);
    public static void main(String[] args) {
        //2、创建生产者线程
        Thread producer = new Thread(()->{
            int goods = 1;
            //3、生产者开始不停的生产
            while (true){
                System.out.println("生产了商品:"+goods);
                try {
                    //4、将生产的商品先放入阻塞队列中
                    queue.put(goods);
                    goods++;
                    //等待10ms生产一个,不然打印太快
                    TimeUnit.MICROSECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        producer.start();
        //5、创建消费者
        Thread customer = new Thread(()->{
            while (true){
                //6、消费者开始从阻塞队列中获取商品
                try {
                    int goods = (int) queue.take();
                    System.out.println("消费了商品:"+goods);
                    // 休眠1秒
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        customer.start();
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/518154.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何注册appuploader账号​

如何注册appuploader账号​ 我们上一篇讲到appuploader的下载安装&#xff0c;要想使用此软件呢&#xff0c;需要注册账号才能使用&#xff0c;今​ 天我们来讲下如何注册appuploader账号来使用软件。​ 1.Apple官网注册Apple ID​ 首先我们点击首页左侧菜单栏中的“常见网…

为什么企业选择局域网即时通讯软件?局域网即时通讯软件哪家好?

在当今互联网普及的时代&#xff0c;企业内部的沟通对企业管理有着非常重要的意义&#xff0c;即时通讯软件已成为企业工作中广泛采用的沟通工具。 然而&#xff0c;随着企业内部敏感信息通过互联网泄露的频繁发生&#xff0c;例如在工作期间&#xff0c;企业员工自发地频繁使…

盘点四款免费在线采购管理系统

今天来盘点五款免费在线采购管理系统。中小型企业在选择采购管理系统时成本是需要考虑的重要因素之一&#xff0c;因此免费在线的采购管理系统是最合适的第一步选择&#xff0c;本文将为您盘点免费在线采购管理系统&#xff1a;1.简道云&#xff1b;2.甄云&#xff1b;3.携客云…

【正点原子STM32连载】 第九章 STM32启动过程分析 摘自【正点原子】STM32F103 战舰开发指南V1.2

1&#xff09;实验平台&#xff1a;正点原子stm32f103战舰开发板V4 2&#xff09;平台购买地址&#xff1a;https://detail.tmall.com/item.htm?id609294757420 3&#xff09;全套实验源码手册视频下载地址&#xff1a; http://www.openedv.com/thread-340252-1-1.html 第九章…

Redis可持久化详解1

目录 Redis可持久化 以下是RDB持久化的代码示例&#xff1a; 面试常问 1什么是Redis的持久化机制&#xff1f; 2Redis支持哪些持久化机制&#xff1f;它们有什么区别&#xff1f; 3Redis的RDB持久化机制的原理是什么&#xff1f; 4Redis的AOF持久化机制的原理是什么&…

《三》包管理工具 npm

包管理工具 npm&#xff1a; npm&#xff1a;Node Package Manager&#xff0c;Node 包管理器&#xff0c;目前已经不仅仅作为 Node 的包管理工具&#xff0c;也作为前端的包管理工具来管理包。 npm 管理的包是存放在一个名为 registry 的仓库中的&#xff0c;发布一个包时是…

分享推荐32位MCU低成本替换8/16位升级完美之选——MM32G0001

灵动微在嵌入式闪存技术上做了优化&#xff0c;采用更稳定和经大规模量产验证的12寸晶圆工艺平台&#xff0c;对产品的功能、性能和成本进行了全方位的打磨&#xff0c;在保持MM32品质目标的前提下&#xff0c;推出了这款极具性价比、低成本的MM32G0001系列MCU产品。 不同于市…

Nuxt学习笔记

创建项目 npx create-nuxt-app projectName SSR 渲染流程 客户端发送 URL 请求到服务端&#xff0c;服务端读取对应的 URL 的模板信息&#xff0c;在服务端做出 HTML 和数据的渲染&#xff0c;渲染完成之后返回整个 HTML 结构给客户端。所以用户在浏览首屏的时候速度会比较快…

Scala学习(四)

文章目录 1.闭包2.函数式编程递归和尾递归2.1递归2.2 尾递归 3.控制抽象3.1 值调用3.2 名调用 4.惰性函数 1.闭包 如果一个函数&#xff0c;访问到了它的外部(局部)变量的值&#xff0c;那么这个函数和它所处的环境称之为闭包 //闭包练习def sumX(x:Int){def sumY(y:Int):Int{…

闲谈【Stable-Diffusion WEBUI】的插件:模型工具箱:省空间利器

文章目录 &#xff08;零&#xff09;前言&#xff08;一&#xff09;模型工具箱&#xff08;Model Toolbox&#xff09;&#xff08;1.1&#xff09;基本使用界面&#xff08;1.2&#xff09;高阶使用界面&#xff08;1.3&#xff09;自动修剪模型 &#xff08;零&#xff09;…

MyBatis基础介绍

目录 MyBatis是什么 怎么学MyBatis 第一个MyBatis查询 MyBatis的定位 创建数据库和表 搭建MyBatis环境 添加MyBatis框架支持 设置MyBatis的配置信息 设置数据库连接的相关信息 配置MyBatis xml的保存路径和xml命名规范 MyBatis模式开发 创建一个实体类 创建MyBatis…

AI换脸系统开发源码交付

AI换脸系统软件的发展趋势包括以下几个方面&#xff1a; 定制化和智能化&#xff1a;随着用户需求的不断增加&#xff0c;AI换脸系统将向更加定制化和智能化的方向发展&#xff0c;通过数据分析和用户画像等手段&#xff0c;为用户提供更加个性化的服务。 多模态应用&a…

通达信头肩底形态选股公式,突破波峰发出信号

本文将为大家介绍头肩底形态选股公式的编写方法&#xff0c;相较于前两篇文章介绍的N字形态和W底形态&#xff0c;头肩底形态更为复杂&#xff0c;包含3个波谷和2个波峰。 头肩底是一种反转形态&#xff0c;在下降趋势之后形成&#xff0c;其完成标志着趋势的改变。该形态包含三…

谷歌浏览器 | Chrome DevTools系统学习篇-Device Mode

大家好&#xff0c;文接上回谷歌浏览器 | Chrome DevTools系统学习篇-概述。所谓“工欲善其事&#xff0c;必先利其器”&#xff0c;我们进一步来熟悉谷歌开发者工具。今天分享的是Device Mode&#xff0c;使用设备模式来估算您的页面在移动设备上的外观和性能。 设备模式是 Ch…

java顺序表——ArrayList详解

1.顺序表的概念 顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构&#xff0c;一般情况下采用数组存储。在数组上完成数据的增删查改。 2.自己实现一个顺序表——MyArrayList 2.1 顺序表成员变量的定义 public class MyArrayList {public static int FEFAU…

优思学院|什么是精益生产?企业如何实现精益生产?

简介 在现代工业社会中&#xff0c;企业的生产效率和质量管理是其生存和发展的关键因素之一。而精益生产作为一种高效的生产管理模式&#xff0c;已经成为了众多企业提升效率和质量的首选。优思学院[1]在本文将对精益生产进行详细的介绍&#xff0c;并提供企业实现精益生产的实…

【Java零基础入门篇】第 ④ 期 - 继承(二)

博主&#xff1a;命运之光 专栏&#xff1a;JAVA入门 学习目标 1.掌握继承性的主要作用、实现、使用限制&#xff1b; 2.掌握this和super的含义及其用法&#xff1b; 3.掌握方法覆写的操作&#xff1b; 4.掌握final关键字的使用&#xff1b; 5.掌握类变量、实例变量和局部变量的…

【应用场景详解】Web自动化测试适用于哪些场景?看完这篇文章你就知道了

【从入门到实战】WEB自动化测试基础教程&#xff0c;手把手教你封装自己的测试框架&#xff01; 目录 前言&#xff1a; 一、什么是web自动化测试&#xff1f; 二、Web自动化测试的应用场景 三、Web自动化测试的实现方法 1.安装Selenium 2.编写测试用例 四、Web自动自动…

第十二章 使用DHCP动态管理主机地址

文章目录 第十二章 使用DHCP动态管理主机地址一、动态主机地址管理协议1、DHCP简介2、DHCP常见术语 二、部署DHCP服务程序1、安装DHCP服务程序2、配置文件参考模板3、dhcpd服务程序配置文件中常见参数及作用 三、自动管理IP地址1、机房所用的网络地址以及参数信息2、关闭虚拟网…

原装二手Anritsu S331E安立S331L 手持式电缆和天线分析仪

Anritsu S331E Site Master 手持式电缆和天线分析仪涵盖 2 MHz 至 4 GHz 频谱&#xff0c;是无线基站电缆和天线系统安装、配置、维护和故障排除的行业标准。Site Master 准确、多功能、价格合理、坚固耐用&#xff0c;是真正的手持设备&#xff0c;重量不到 5 磅&#xff08;包…