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

news2024/9/26 3:25:47

文章目录

  • 多线程设计模式
    • 什么是设计模式
    • 单例模式
    • 饿汉模式
    • 懒汉模式
    • 线程安全问题
      • 懒汉模式就一定安全吗?
      • 锁引发的效率问题
      • 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;
        }
    }
}

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

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

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

相关文章

网络套接字-TCP服务器

一 前言 前面已经写过udp服务器的实现了&#xff0c;那里说了很多编写服务器的所需知识&#xff0c;在tcp服务器实现中就不再赘述了。 二 服务端编写 大致接口如下。 ./server port端口号 启动时指明端口号 void usage(const std::string proc) {std::cout<<"Usa…

Py之scikit-learn-extra:scikit-learn-extra的简介、安装、案例应用之详细攻略

Py之scikit-learn-extra&#xff1a;scikit-learn-extra的简介、安装、案例应用之详细攻略 目录 scikit-learn-extra的简介 scikit-learn-extra的安装 scikit-learn-extra的案例应用 1、使用 scikit-learn-extra 中的 IsolationForest 模型进行异常检测 scikit-learn-extra…

Orbit 使用指南 10|在机器人上安装传感器 | Isaac Sim | Omniverse

如是我闻&#xff1a; 资产类&#xff08;asset classes&#xff09;允许我们创建和模拟机器人&#xff0c;而传感器 (sensors) 则帮助我们获取关于环境的信息&#xff0c;获取不同的本体感知和外界感知信息。例如&#xff0c;摄像头传感器可用于获取环境的视觉信息&#xff0c…

【小沐学Python】Python实现Web图表功能(Lux)

文章目录 1、简介2、安装3、测试3.1 入门示例3.2 入门示例2 结语 1、简介 https://github.com/lux-org/lux 用于智能可视化发现的 Python API Lux 是一个 Python 库&#xff0c;通过自动化可视化和数据分析过程来促进快速简便的数据探索。通过简单地在 Jupyter 笔记本中打印出…

我的风采——android studio

目录 实现“我的风采”页面要求理论代码生成apk文件 实现“我的风采”页面 要求 要求利用’java框架的边框布局实现“找的风采 ”页而&#xff0c;其中中间为你的生活照&#xff0c;左右和下面为按钮&#xff0c;上面为标签 理论 Java GUI编程是Java程序设计的重要组成部分…

QT(C++)-error LNK2038: 检测到“_ITERATOR_DEBUG_LEVEL”的不匹配项: 值“2”不匹配值“0”

1、项目场景&#xff1a; 在VS中采用QT&#xff08;C&#xff09;调试时&#xff0c;出现error LNK2038: 检测到“_ITERATOR_DEBUG_LEVEL”的不匹配项: 值“2”不匹配值“0”错误 2、解决方案&#xff1a; 在“解决方案资源管理器”中选中出现此类BUG的项目&#xff0c;右键-…

uniapp-Form示例(uviewPlus)

示例说明 Vue版本&#xff1a;vue3 组件&#xff1a;uviewPlus&#xff08;Form 表单 | uview-plus 3.0 - 全面兼容nvue的uni-app生态框架 - uni-app UI框架&#xff09; 说明&#xff1a;表单组建、表单验证、提交验证等&#xff1b; 截图&#xff1a; 示例代码 <templat…

PCIe总线-PCIe总线简介(一)

1.概述 早期的计算机使用PCI&#xff08;Peripheral Component Interconnect&#xff09;总线与外围设备相连&#xff0c;PCI总线使用单端并行信号进行数据传输&#xff0c;由于单端信号很容易被外部系统干扰&#xff0c;其总线频率很难进一步提高。目前&#xff0c;为了提高总…

k8s笔记27--快速了解 k8s pod和cgroup的关系

k8s笔记27--快速了解 k8s pod和 cgroup 的关系 介绍pod & cgroup注意事项说明 介绍 随着云计算、云原生技术的成熟和广泛应用&#xff0c;K8S已经成为容器编排的事实标准&#xff0c;学习了解容器、K8S技术对于新时代的IT从业者显得极其重要了。 之前在文章 docker笔记13–…

UDS升级入门,手把手教你——开篇

前面关于OTA的文章&#xff0c;写的比较乱&#xff0c;索性整了一个专栏&#xff0c;来认真梳理下&#xff0c;话不多开整。 准备工作&#xff1a; 1、QT环境 上位机开发 2、MDK环境&#xff0c;STM32F103&#xff0c;vscode MCU开发环境&#xff0c;调试 3、JFlash环境安…

【C语言数据结构】排序

1.排序的概念 在深入研究各个排序算法之前&#xff0c;首先&#xff0c;我们要对排序有个大概的了解&#xff0c;即与排序相关的一些概念 Q&#xff1a;什么是排序&#xff1f; A&#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小…

基于Java中的SSM框架实现考研指导平台系统项目【项目源码+论文说明】

基于Java中的SSM框架实现考研指导平台系统演示 摘要 应对考研的学生&#xff0c;为了更好的使校园考研有一个更好的环境好好的学习&#xff0c;建议一个好的校园网站&#xff0c;是非常有必要的。提供学生的学习提供一个交流的空间。帮助同学们在学习高数、学习设计、学习统计…

使能 Linux 内核自带的 FlexCAN 驱动

一. 简介 前面一篇文章学习了 ALPHA开发板修改CAN的设备树节点信息&#xff0c;并加载测试过设备树文件&#xff0c;文件如下&#xff1a; ALPHA开发板修改CAN的设备树节点信息-CSDN博客 本文是学习使能 IMX6ULL的 CAN驱动&#xff0c;也就是通过内核配置来实现。 二. 使能…

Spring Cloud五:Spring Cloud与持续集成/持续部署(CI/CD)

Spring Cloud一&#xff1a;Spring Cloud 简介 Spring Cloud二&#xff1a;核心组件解析 Spring Cloud三&#xff1a;API网关深入探索与实战应用 Spring Cloud四&#xff1a;微服务治理与安全 文章目录 一、Spring Cloud在CI/CD中的角色1. 服务注册与发现&#xff1a;自动化管理…

YOLOV5 部署:TensorRT的安装和使用

1、介绍 TensorRT 可以加速神经网络的推理时间,常常在工业生产中使用 因为TensorRT需要使用到cuda和cudnn加速,所以需要安装这两个,安装的具体步骤参考前文: YOLOV5 部署:cuda和cuDNN安装-CSDN博客 2、TensorRT 下载 TensorRT下载地址:NVIDIA TensorRT Download | NV…

分类预测 | Matlab实现CNN-LSTM-Mutilhead-Attention卷积神经网络-长短期记忆网络融合多头注意力机制多特征分类预测

分类预测 | Matlab实现CNN-LSTM-Mutilhead-Attention卷积神经网络-长短期记忆网络融合多头注意力机制多特征分类预测 目录 分类预测 | Matlab实现CNN-LSTM-Mutilhead-Attention卷积神经网络-长短期记忆网络融合多头注意力机制多特征分类预测分类效果基本介绍模型描述程序设计参…

初识kafka-数据存储篇1

目录 背景 1 kafka总体体系结构 2 疑问解答 2.1 高吞吐低延迟 2.2 实现分布式存储和数据读取 2.3 如何保证数据不丢失 背景 最近在和产品过项目审批的时候&#xff0c;深刻感受到业务方对系统的时时响应提出了更高的要求。目前手上大部分的业务都是基础定时任务去实现的&…

[Java基础揉碎]单例模式

目录 什么是设计模式 什么是单例模式 饿汉式与懒汉式 饿汉式vs懒汉式 懒汉式存在线程安全问题 什么是设计模式 1.静态方法和属性的经典使用 2.设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、 以及解决问题的思考方式。设计模式就像是经典的棋谱&am…

使用 RunwayML 对图像进行 Camera 操作

RunwayML 是一個功能強大的平台&#xff0c;可以讓您使用 AI 和机器学习来增强您的图像和视频。 它提供一系列预训练模型&#xff0c;可用于各种任务&#xff0c;包括图像编辑、风格化和特效。 在本文中&#xff0c;我们将介绍如何使用 RunwayML 对图像进行 Camera 操作。我们…

游戏引擎中的地形系统

一、地形的几何 1.1 高度图 记录不同定点的高度&#xff0c;对每个网格/顶点应用高度、材质等信息&#xff0c;我们每个顶点可以根据高度改变位移 但是这种方法是不适用于开放世界的。很难直接画出几百万公里的场景 1.2 自适应网格细分 当fov越来越窄的时候&#xff0c;网格…