JAVAEE——多线程的设计模式,生产消费模型,阻塞队列

文章目录

  • 多线程设计模式
    • 什么是设计模式
    • 单例模式
    • 饿汉模式
    • 懒汉模式
    • 线程安全问题
      • 懒汉模式就一定安全吗?
      • 锁引发的效率问题
      • jvm的优化引起的安全问题
  • 阻塞队列
    • 阻塞队列是什么?
    • 生产消费者模型
    • 阻塞队列实现消费生产者模型可能遇到的异常

多线程设计模式

什么是设计模式

首先我们要先明白什么是设计模式呢?举个栗子,设计模式就像我们下棋的棋谱一样按照某种需求按照一定的规则来进行特定的应对软件开发中也有很多情景。因此大佬们总结了一套经典的设计模式其中面试经常问的当然就是单例模式了

单例模式

什么是单列模式呢?单列模式字面意思我们拆开来看
什么时单呢?单就是单一,一个的意思。列是什么呢?就是实例。合起来就是一个实例,也就是说这个类只能实列化出一个对象,那么该怎么实现这样的方式呢?其实很简单我们只需要把构造方法搞成私有的就可以了那么代码如下

class Mytest{
    private static Mytest mytest=new Mytest();
    private Mytest(){

    }
    private Mytest getMytest(){
        return mytest;
    }
}
public class Main {
    public static void main(String[] args) {

    }
}

这样子我们就可以做到不能自己创建对象而只能通过getMytest()获取已经创建好的对象。那么这时候就涉及到两种模式了就是饿汉模式和懒汉模式

饿汉模式

饿汉模式是什么呢?其实就是我们上面的那种代码,就是即使我们现在还没有调用这个类还不需要这个类的对象我们都已经把他实列化出来了一个对象了这就是饿汉模式。也就是当我们即使没用到这个实例的对象也先把对象创建好就像一个饿汉一样扑到饭上。

懒汉模式

说完了饿汉模式我们来讲一下懒汉模式,什么是懒汉模式呢?我们对比一下饿汉的概念来类比,懒汉就是当我们需要这个类的对象的 时候再给我们实列化出来代码如下

class Mytest{
    private static Mytest mytest;
    private Mytest(){

    }
    private Mytest getMytest(){
        if(mytest==null){
            mytest=new Mytest();
        }
        return mytest;
    }
}
public class Main {
    public static void main(String[] args) {

    }
}

代码就是像上面这样,先判断一下对象是否被创建,如果没有被创建那么就实例化处对象并将对象返回如果已经创建的话那就把创建好的对象直接返回让其使用。

线程安全问题

那么讲到这里我们来思考一下,懒汉模式和饿汉模式哪个是线程安全的呢?其实懒汉模式是线程安全的,因为我们可以看一下饿汉模式代码如下

class Mytest{
    private static Mytest mytest=new Mytest();
    private Mytest(){}
    public static Mytest  getMytest(){
        return mytest;
    }
}
public class Main {
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            Mytest ty=Mytest.getMytest();
        });
        Thread t2=new Thread(()->{
            Mytest ty2=Mytest.getMytest();
        });
        t1.start();
        t2.start();
    }
}

当我们使用饿汉模式的时候我们两个线程在分别调用Mytest的时候就会导致我们两个线程创建的mytest是不一样的,我们要明白一件事情就是当一个资源被多个线程即读取又修改的时候那么它多半其实就是不安全的。当一个资源只是被读取的时候那么它也就是安全的。这时候我们再来看懒汉模式就会发现我们加了一个if就会使得当我们第一次创建好这个对象之后后续的线程是无法更改这个对象的,因此他就是线程安全的。

懒汉模式就一定安全吗?

可是我们要知道一个事情就是懒汉模式就一定安全吗?其实不是的,我们上面说的只是相对安全而已。那么为什么懒汉也是不安全的呢?其实是因为我们创建对象的过程他不是一个原子性的过程他是分成了几个步骤的
new对象的步骤分为三步:

  1. 分配内存
  2. 构造对象
  3. 赋值给对象引用

那么当我们执行这三步的时候其实就会有之前跟++类似的过程,我们画图来解释一下。
在这里插入图片描述
我们来举个例子帮助大家更好的了解一下。
在这里插入图片描述
那么这时候有什么办法可以解决这个不稳定因素呢?很简单就是加锁就可以了。代码如下

class Mytest{
    public static Object ob=new Object();
    private static Mytest mytest=null;
    private Mytest(){}
    public static Mytest  getMytest(){
        synchronized (ob){
            if(mytest==null){
                mytest=new Mytest();
            }
        }
        return mytest;
    }
}
public class Main {
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            Mytest ty=Mytest.getMytest();
        });
        Thread t2=new Thread(()->{
            Mytest ty2=Mytest.getMytest();
        });
        t1.start();
        t2.start();
    }
}

那么加锁后上面的过程就变成了下面这样。

class Mytest{
    public static Object ob=new Object();
    private static Mytest mytest=null;
    private Mytest(){}
    public static Mytest  getMytest(){
        synchronized (ob){
            if(mytest==null){
                mytest=new Mytest();
            }
        }
        return mytest;
    }
}
public class Main {
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            Mytest ty=Mytest.getMytest();
        });
        Thread t2=new Thread(()->{
            Mytest ty2=Mytest.getMytest();
        });
        System.out.println(e);
        t2.start();

    }
}

那么这时候我们的代码就做到了线程安全,可是还有一个问题就是效率问题

锁引发的效率问题

这时候我们再来思考一下这个代码的进程。首先t1线程获取锁,然后开始创建对象,t2线程在t1线程还没有结束之前就无法获取到这把锁那么这时候就需要去等待,可是这时候就有一个问题那就是说假如我们有100个线程都需要使用这个对象那么都需要先判断一个这个对象是否被创建那么这时候就需要轮着去申请锁释放锁,我们要知道一个事情那就是申请锁释放锁这个过程是非常消耗时间的,因此如果一个代码涉及到多次对锁的释放和申请的话那么这个代码注定与高效率无缘了。那么该怎么办去改善效率问题呢?很简单我们只需要再加一个if就可以了

class Mytest{
    public static Object ob=new Object();
    private static Mytest mytest=null;
    private Mytest(){}
    public static Mytest  getMytest(){
        if(mytest==null){
            synchronized (ob){
                if(mytest==null){
                    mytest=new Mytest();
                }
            }
        }
        return mytest;
    }
}
public class Main {
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            Mytest ty=Mytest.getMytest();
        });
        Thread t2=new Thread(()->{
            Mytest ty2=Mytest.getMytest();
        });
        System.out.println(e);
        t2.start();

    }
}

那么就有人有疑问了因为刚刚说过我们的new不是一个原子性的操作如果说我们第一个线程在创建对象的期间那么这个对象的引用就还是空的这时候其余的线程还是可以通过第一个if的然后那不还是需要去等待锁释放锁吗?所以加个if有什么用呢?其实很有这是很有道理的,但是大家可以想一下这是不是只存在这个对象还没被创建的时期,如果这个对象已经被创建的话那么其余的线程就无法再去获取这把锁了我们避免的是当对象已经创建好后,后续线程想要调用这个引用还需要去获取锁的这种情况。
也就是下面的这个过程
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

jvm的优化引起的安全问题

在我们多线程创建的过程中我们上面提到了一个事情就是其实new的过程并不是原子的过程,而这其中呢jvm是有优化的也就是说正常来说我们的 步骤应该是

  1. 分配内存
  2. 构造对象
  3. 赋值给对象引用

但是由于线程的优化导致我们的过程可能就变成了 1 3 2,也就是

  1. 分配内存
  2. 赋值给对象引用
  3. 构造对象

然后当一个对象执行到赋值给对象引用的时候那么这时候我们代码中的mytest就已经不是空的了。也就是说这时候就有可能导致我们的if循环不会进去阻塞而是把还没有完全创建好的对象直接给我们返回比如下面的这个示意图
在这里插入图片描述
那么这时候该怎么解决呢?那就是加一个volatile
在这里插入图片描述
修改后的代码如下

class Mytest{
    public static volatile Object ob=new Object();
    private static Mytest mytest=null;
    private Mytest(){}
    public static Mytest  getMytest(){
        if(mytest==null){
            synchronized (ob){
                if(mytest==null){
                    mytest=new Mytest();
                }
            }
        }
        return mytest;
    }
}
public class Main {
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            Mytest ty=Mytest.getMytest();
        });
        Thread t2=new Thread(()->{
            Mytest ty2=Mytest.getMytest();
        });
        System.out.println(e);
        t2.start();

    }
}

阻塞队列

阻塞队列是什么?

首先我们要先明白阻塞队列是什么呢?阻塞队列其实就是一种特殊的队列他也是按照先进先出的顺序的,但是他跟普通队列的区别是什么呢?其实就是线程的安全性,当我们学到了多线程后我们就要明白,一个队列在未来可能不只是一个线程再往里面填充元素,也不一定是一个线程再往里面移除元素,因此线程安全性就很重要了。那么它的特点就体现在以下方面

  • 当队列满了的时候放入元素就会堵塞
  • 当队列空的时候移除元素就会堵塞
  • 当队列放入元素正在阻塞的时候移除一个元素可以解除其放入元素堵塞的情况
  • 当对列移除元素为空的时候添加一个元素就可以解除其移除元素堵塞的情况。

阻塞队列的一个典型应用场景就是 “生产者消费者模型”. 这是一种非常典型的开发模型.

生产消费者模型

什么是生产消费者模型呢?我们用阻塞队列为列将两者结合起来进行讲解,我们可以把阻塞队列看成一个钱包那么这时候有两个线程。生产线程和消费线程。
在这里插入图片描述
那么这就是一个生产消费者模型,生产线程负责往里放元素,消费线程负责往里取出元素也就是在消费。
那么当时说的阻塞到底是怎么实现的呢我们来看一下下面的这个代码。

import java.util.concurrent.BlockingQueue;

public class MyBlockQueue {
    public String[] BlockQueue=new String[100];
    private int tail=0;
    private int head=0;
    int size=0;
    public void put(String elem){
        synchronized (this){
            if(size==BlockQueue.length){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    //throw new RuntimeException(e);
                }
            }
            BlockQueue[tail++]=elem;
            if(tail==BlockQueue.length){
                tail=0;
            }
            size++;
        }
    }
    public String take(){
        synchronized (this){
            if(size==0){
               return null;
            }
            String ret=BlockQueue[head];
            head++;
            if(head==BlockQueue.length){
                head=0;
            }
            size--;
            this.notify();
            return ret;
        }
    }
}

那么现在我们来解读以下这个代码这个代码中呢假如了锁,具体的意思就是说当我们put的时候假如说我们的这个队列已经满了的话那么我们这时候生产线程就会陷入等待直到我们的消费线程将这个元素取出来之后,才会将其唤醒从而继续执行但是这里面我们为什么要进行抛出异常呢?

阻塞队列实现消费生产者模型可能遇到的异常

这里面为什么我们要加上抛出异常呢?因为我们要知道一个事情那就是唤醒线程不止是notify可以唤醒还有一种唤醒方式那就是intrrupt。当我们的intrrupt方法唤醒线程的时候就会导致一个问题那就是我们的出现bug,因为我们的阻塞队列是模拟的循环队列进行的因此当队列满了之后却不通过正确的途径去将其启动的话,就会导致我们的前面插入的元素被后面插入的元素覆盖掉因此这时候就需要我们进行一些手段来预防,那么该怎么办呢?其实interrupt进行线程启动的时候是会导致抛出异常的我们只需要对异常进行捕获就可以了那么代码如下

public class Main {
    public static void main(String[] args) {
        MyBlockQueue mytest=new MyBlockQueue();
        Thread t1=new Thread(()->{
            int num=0;
            while(true){
                mytest.put("生产者生产了"+num+"元素");
                System.out.println("生产者生产了" + num + "元素");
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                num++;
            }
        });
        Thread t2=new Thread(()->{
            int num=0;
            while(true){
                String ret=mytest.take();
                System.out.println("消费者消费了这个"+ret+"元素");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t1.start();
        //t2.start();
        t1.interrupt();
    }
}

上面的代码确实可以解决这样的一个问题可是在实际开发中我们会感觉这样子是不是太粗暴了毕竟我只是操作失误但是却直接抛出异常,代码终止如果我们不希望这么暴力怎么办呢?其实很简单只需要在唤醒的之后再加个if就可以了如下图
在这里插入图片描述
但是这样就可以了吗当然不是这时候是两个线程假如说是有多个线程呢?那么该怎么办难道无限if套下去?当然不是,我们可以加个while循环啊
在这里插入图片描述
这里解释以下wait的异常我们还是需要捕获的但是可以不做处理
我们的运行截图就变成了
在这里插入图片描述
这个样子也就是当我们的长度到达了我们设置的长度之后就停止运行了。
像这样那么我们的代码就变成了下面这样

import java.util.concurrent.BlockingQueue;

public class MyBlockQueue {
    public String[] BlockQueue=new String[100];
    private int tail=0;
    private int head=0;
    int size=0;
    public void put(String elem){
        synchronized (this){
           while(size==BlockQueue.length){
               try {
                   this.wait();
               } catch (InterruptedException e) {

               }
           }
            BlockQueue[tail++]=elem;
            if(tail==BlockQueue.length){
                tail=0;
            }
            size++;
        }
    }
    public String take(){
        synchronized (this){
            if(size==0){
               return null;
            }
            String ret=BlockQueue[head];
            head++;
            if(head==BlockQueue.length){
                head=0;
            }
            size--;
            this.notify();
            return ret;
        }
    }
}

爱人是这个寒冷的世界上的一束温暖的阳光。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【PHP + 代码审计】数组函数

🍬 博主介绍👨‍🎓 博主介绍:大家好,我是 hacker-routing ,很高兴认识大家~ ✨主攻领域:【渗透领域】【应急响应】 【Java、PHP】 【VulnHub靶场复现】【面试分析】 🎉点赞➕评论➕收…

安全工具介绍 SCNR/Arachni

关于SCNR 原来叫Arachni 是开源的,现在是SCNR,商用工具了 可试用一个月 Arachni Web Application Security Scanner Framework 看名字就知道了,针对web app 的安全工具,DASTIAST吧 安装 安装之前先 sudo apt-get update sudo…

力扣面试150 阶乘后的零 数论 找规律 质因数

Problem: 172. 阶乘后的零 思路 👨‍🏫 大佬神解 一个数末尾有多少个 0 ,取决于这个数 有多少个因子 10而 10 可以分解出质因子 2 和 5而在阶乘种,2 的倍数会比 5 的倍数多,换而言之,每一个 5 都会找到一…

AWTK T9 输入法实现原理

1. T9 输入法的中文字典数据 网上可以找到 T9 输入法的中文字典数据,但是通常有两个问题: 采用 GPL 协议,不太适合加入 AWTK。 只支持单个汉字的输入,不支持词组的输入。 经过考虑之后,决定自己生成 T9 输入法的中…

选择器加练习

一、常用的选择器 1.元素选择器 语法 : 标签名{} 作用 : 选中对应标签中的内容 例:p{} , div{} , span{} , ol{} , ul{} ...... 2.类选择器(class选择器) 语法 : .class属性值{} 作用 : 选中对应class属性值的元素 注意:class里面的属性值不能以数字开头,如果以符号开头,…

基于python+vue城市交通管理系统的设计与实现flask-django-php-nodejs

此系统设计主要采用的是python语言来进行开发,采用django/flask框架技术,框架分为三层,分别是控制层Controller,业务处理层Service,持久层dao,能够采用多层次管理开发,对于各个模块设计制作有一…

Docker 镜像仓库

目录 1、搭建私有 registry 服务端创建镜像仓库 客户端推送镜像 镜像导入导出 2、Nginx 代理 registry 仓库 SSL 证书 & https 协议 SSL证书 https协议 SSL 的验证流程 客户端安装 Nginx 使用 openssl 生成CA根证书和根证书key 创建 Nginx 服务证书 配置启动 N…

线段树和树状数组

📟作者主页:慢热的陕西人 🌴专栏链接:力扣刷题日记 📣欢迎各位大佬👍点赞🔥关注🚓收藏,🍉留言 文章目录 树状数组和线段树1.树状数组1.1动态求连续区间和1.2数…

c#矩阵求逆

目录 一、矩阵求逆的数学方法 1、伴随矩阵法 2、初等变换法 3、分块矩阵法 4、定义法 二、矩阵求逆C#代码 1、伴随矩阵法求指定3*3阶数矩阵的逆矩阵 (1)伴随矩阵数学方法 (2)代码 (3)计算 2、对…

Unity Shader

练习项目链接 1. Shader 介绍 Shader其实就是专门用来渲染图形的一段代码,通过shader,可以自定义显卡渲染画面的算法,使画面达到我们想要的效果。小到每一个像素点,大到整个屏幕,比如下面这个比较常见的效果。 2. Sh…

javaSwing宿舍管理系统(三个角色)

一、 简介 宿舍管理系统是一个针对学校宿舍管理的软件系统,旨在方便学生、宿管和管理员进行宿舍信息管理、学生信息管理以及宿舍评比等操作。该系统使用 Java Swing 进行界面设计,分为三个角色:管理员、宿管和学生。 二、 功能模块 2.1 管…

RK3568平台 iperf3测试网络性能

一.iperf3简介 iperf是一款开源的网络性能测试工具,主要用于测量TCP和UDP带宽性能。它可以在不同的操作系统上运行,包括Windows、Linux、macOS等。iperf具有简单易用、功能强大、高度可配置等特点,广泛应用于网络性能测试、网络故障诊断和网…

深度学习绘制热力图heatmap、使模型具有可解释性

思路 获取想要解释的那一层的特征图,然后根据特征图梯度计算出权重值,加在原图上面。 Demo 加上类激活(cam) 可以看到,cam将模型认为有利于分类的特征标注了出来。 下面以ResNet50为例: Trick: 使用 for i in model._modules.items():可以…

二十三 超级数据查看器 讲解稿 设置

二十三 超级数据查看器 讲解稿 设置 ​点击此处 以新页面 打开B站 播放当前教学视频 点击访问app下载页面 百度手机助手 下载地址 大家好,这节课我们讲一下,超级数据查看器的设置功能。 首先,我们打开超级数据查看器, 我…

2023年全国青少年信息素养大赛(python)初赛真题

选择题(每题5分,共20题,满分100分) 1、关于列表的索引,下列说法正确的是? A.列表的索引从0开始 B.列表的索引从1开始 C.列表中可能存在两个元素的索引一致 D&#xff0…

第四百一十九回

文章目录 1. 概念介绍2. 思路与方法2.1 实现思路2.2 实现方法 3. 示例代码4. 内容总结 我们在上一章回中介绍了"自定义标题栏"相关的内容,本章回中将介绍自定义Action菜单.闲话休提,让我们一起Talk Flutter吧。 1. 概念介绍 我们在这里提到的…

web自动化3-pytest前后置夹具

一、pytest前后置(夹具)-fixture 夹具的作用:在用例执行之前和之后,需要做的准备工作和收尾工作。 用于固定测试环境,以及清理回收资源。 举个例子:访问一个被测页面-登录页面,执行测试用例过…

SpringCloud-Gateway服务网关

一、网关介绍 1. 为什么需要网关 Gateway网关是我们服务的守门神,所有微服务的统一入口。 网关的核心功能特性: 请求路由 权限控制 限流 架构图: 权限控制:网关作为微服务入口,需要校验用户是是否有请求资格&am…

算法-双指针

目录 1、双指针遍历分割:避免开空间,原地处理 2、快慢指针:循环条件下的判断 3、左右指针(对撞指针):分析具有单调性,避免重复计算 双指针又分为双指针遍历分割,快慢指针和左右指针 1、双指…

深度学习 tablent表格识别实践记录

下载代码:https://github.com/asagar60/TableNet-pytorch 下载模型:https://drive.usercontent.google.com/download?id13eDDMHbxHaeBbkIsQ7RSgyaf6DSx9io1&exportdownload&confirmt&uuid1bf2e85f-5a4f-4ce8-976c-395d865a3c37 原理&#…