深度学习设计模式之单例模式

news2024/12/23 11:25:49

一、单例模式简介

一个类只能有一个实例,提供该实例的全局访问点;

二、单例模式实现步骤

使用一个私有构造函数、一个私有静态变量以及一个公有静态函数来实现。
私有构造函数保证了不能通过构造函数来创建对象实例,只能通过公有静态函数返回唯一的私有静态变量。
在这里插入图片描述

三、单例模式的两种方式

在这里插入图片描述

1.懒汉模式

懒汉模式,通俗来讲就是只有饿的时候,才会去找饭吃。通常只有对象被需要的时候才会去创建。最显而易见的优点就是,节省资源。如果没有地方用到这个类,这个类将不会进行实例化。

1.1 简易版懒汉模式

public class LzaySingleton {

    /**
     * 成员变量
     */
    private static LzaySingleton lzaySingleton;

    /**
     * 构造方法私有化
     */
    private LzaySingleton(){

    }

    /**
     * 获取单例对象
     * @return
     */
    public static LzaySingleton  getInstance(){
        if(lzaySingleton == null){
            System.out.println("创建实例");
            lzaySingleton =  new LzaySingleton();
            return lzaySingleton;
        }
        System.out.println("实例对象已存在,无需再创建");
        return lzaySingleton;
    }

}

测试类

    public static void main(String[] args) {
        // 先创建一个对象,看是否有输出
        LzaySingleton lzaySingleton = LzaySingleton.getInstance();

        LzaySingleton lzaySingleton1 = LzaySingleton.getInstance();

    }

结果:
在这里插入图片描述
简易版的单例模式存在的问题就是:在多线程的情况下是不安全的,会打破单例的定义。
例如:有2个线程,线程A,线程B;同时成员变量lzaySingleton为null;线程A,线程B,同时走到if(lzaySingleton == null),那将会执行两次lzaySingleton = new LzaySingleton();就会实例化两次对象,从而打破单例模式的设定。
在这里插入图片描述
怎么解决呢?接下来就是我们另外一种懒汉单例模式登场了。

1.2线程安全的单例模式

怎么解决线程安全?那就很简单了,加锁就可以了。
只需要再getInstance()方法上加 synchronized就行,这样保证同一个时间点,只会有一个线程进入到这个方法,从而解决多次创建实例的问题。

public class LzaySingleton {

    /**
     * 成员变量
     */
    private static LzaySingleton lzaySingleton;

    /**
     * 构造方法私有化
     */
    private LzaySingleton(){

    }

    /**
     * 获取单例对象
     * @return
     */
    public static synchronized LzaySingleton  getInstance(){
        if(lzaySingleton == null){
            System.out.println("创建实例");
            lzaySingleton =  new LzaySingleton();
            return lzaySingleton;
        }
        System.out.println("实例对象已存在,无需再创建");
        return lzaySingleton;
    }

}

以上的方法虽然可以解决多线程的问题,但是往往单例对象的内容逻辑是非常复杂的,使用synchronized 修饰方法,当其他线程进入该方法的时候,就会进入等待,对性能还是有一定影响的。
解决这个问题,可以灵活的使用synchronized

1.3 线程安全的单例模式V2.0版

为了解决synchronized修饰方法带来的系统开销。我们可以通过灵活运用synchronized来解决此问题。众所周知synchronized 加锁是有多种方式的。我们使用代码块的方式,只有再创建对象的时候使用 synchronized

public class LzaySingleton {

    /**
     * 成员变量
     */
    private static LzaySingleton lzaySingleton;

    /**
     * 构造方法私有化
     */
    private LzaySingleton(){

    }

    /**
     * 获取单例对象
     * @return
     */
    public static LzaySingleton  getInstance(){
        if(lzaySingleton == null){
        	//  synchronized 代码块
            synchronized (LzaySingleton.class){
                System.out.println("创建实例");
                lzaySingleton =  new LzaySingleton();
            }
            return lzaySingleton;
        }
        System.out.println("实例对象已存在,无需再创建");
        return lzaySingleton;
    }

}

这种方式虽然解决了,锁粒度问题带来的性能开销问题,但是又有一个致命问题,我们又回到解放前了。
同样的多线程问题,如果线程A,线程B,同时又到了这一步:
在这里插入图片描述
线程A和B拿到的对象都是null,然后线程A侥幸拿到了锁,线程B就只能再外面等待线程A。同样的问题就会再现,线程A执行完lzaySingleton = new LzaySingleton();线程B就会拿到锁,然后再执行一次lzaySingleton = new LzaySingleton();,所有使用synchronized 代码块的方式加锁,还不够完善。

1.3 线程安全的单例模式V2.1版-双重校验锁

因为上面使用了synchronized 代码块的方式加锁,减少了系统的开销,但是也带来了新的问题,因此我们多增加一个判断,如下:

public class LzaySingleton {

    /**
     * 成员变量
     */
    private static LzaySingleton lzaySingleton;

    /**
     * 构造方法私有化
     */
    private LzaySingleton(){

    }

    /**
     * 获取单例对象
     * @return
     */
    public static LzaySingleton  getInstance(){
        if(lzaySingleton == null){
            synchronized (LzaySingleton.class){
                if(lzaySingleton == null){
                    System.out.println("创建实例");
                    lzaySingleton =  new LzaySingleton();
                }
            }
            return lzaySingleton;
        }
        System.out.println("实例对象已存在,无需再创建");
        return lzaySingleton;
    }

}

这样,即使线程A和线程B同时都到了这一步:
在这里插入图片描述
即使A拿到了锁,执行完lzaySingleton = new LzaySingleton();以后,到B执行时也会被这个校验给拦住
在这里插入图片描述
至此高性能加锁的单例模式完成,但是他还不是最终版本,依旧存在一些小问题。

1.3 线程安全的单例模式V3.0版-双重校验锁终极版本

目前代码层面已经解决问题,但是深究底层,时 lzaySingleton = new LzaySingleton();这个操作并不是原子性的,因为底层在编译运行代码的时候,会对当前代码进行优化,会存在指令重排序情况。而 new LzaySingleton() 时至少需要3步才能完成。
1.分配内存空间;
2.实例化对象;
3.将对象指向分配的空间地址;
如果编译的时候进行了指令重排序,本来正常操作时 1 -> 2 -> 3这样,重排序后则可能会出现 1-> 3 -> 2 这个时候,单线程肯定没问题,但是在多线程的情况下,因为对象还没创建完成,其他线程执行到这里的时候,认为对象不为空,已经实例化成功了,就直接获取对象使用了。其实拿到的对象并不是最终的对象,只是一个半成品的,所以使用的过程中,就会出现意想不到的问题。
在这里插入图片描述
这个时候就需要使用 JVM的关键字 volatile 来解决指令重排序的问题了。
简单介绍一下 volatile
1.volatile有3个特性:可见性、有序性、原子性;
可见性是当多个线程同时访问一个变量的时候,其中一个线程修改了变量的值,其他线程能立刻看到修改的变量值。
有序性是禁止了指令重排序,执行程序代码时,按照顺序来执行。
原子性是一个操作是不能中断的,要不全部都执行,要不都不执行。
2. volatile 是用来修饰变量的,无法修饰代码块和方法。
3. volatile的使用:只要修饰一个 可能被多线程同时访问的变量上就行。
详细情况可自行查询相关资料。

最终代码如下: 对成员变量lzaySingleton 进行了volatile 修饰,防止了创建时的指令重排序。

public class LzaySingleton {

    /**
     * 成员变量
     */
    private static volatile LzaySingleton lzaySingleton;

    /**
     * 构造方法私有化
     */
    private LzaySingleton(){

    }

    /**
     * 获取单例对象
     * @return
     */
    public static LzaySingleton  getInstance(){
        if(lzaySingleton == null){
            synchronized (LzaySingleton.class){
                if(lzaySingleton == null){
                    System.out.println("创建实例");
                    lzaySingleton =  new LzaySingleton();
                }
            }
            return lzaySingleton;
        }
        System.out.println("实例对象已存在,无需再创建");
        return lzaySingleton;
    }

}

至此单例模式的懒汉模式最终版完成。

2.饿汉模式

饿汉模式相对来说,比较简单,通俗来说就是,一上来就先去找吃的和懒汉相反。系统加载的时候就初始化对象。优点就是简单,不存在什么多线程问题。缺点就是占用内存。
实现如下:

public class HungrySingleton {
    // 一开始就初始化对象
    private static HungrySingleton hungrySingleton = new HungrySingleton();
    // 私有化构造方法
    private HungrySingleton(){}

    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

四、扩展实现单例

1.使用枚举的方式实现单例模式

使用枚举的方式实现单例模式,是《Effective java》一书中提到的
在这里插入图片描述
上面的几种方式已经实现了单例模式,但是如果碰到特殊的情况,比如反射的时候,通过 setAccessible() 方法还是可以将私有构造函数的访问级别设置为 public,然后调用构造函数从而实例化对象的。使用枚举就天然的解决反射问题。

直接在枚举类里面写功能方法,代码如下:

public enum SingletonEnum {

    INSTANCE
    ;
    public void test(){
        System.out.println("1111");
    }
}

测试类

   public static void main(String[] args) {
        SingletonEnum.INSTANCE.test();

    }

结果
在这里插入图片描述

2.使用内部类的方式实现单例模式

内部类的方式实现单例模式,加载Singleton的时候静态内部类 SingletonHolder 不会被加载。 只有调用 getInstance()方法的时候才会去初始化对象。这种方式不仅具有延迟初始化的好处,而且由虚拟机提供了对线程安全的支持。
以下是代码实现:

/**
 * 静态内部类方式实现单例
 */
public class Singleton {

    // 私有化构造方法
    private Singleton(){}

    public void test(){
        System.out.println("2222");
    }
    /**
     * 静态内部类
     */
    private static class SingletonHolder{
        // 初始化对象
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance(){
        return SingletonHolder.INSTANCE;
    }

}

测试类

    public static void main(String[] args) {
        Singleton.getInstance().test();

    }

结果
在这里插入图片描述

FAQ

1.为什么要私有化构造方法?

单例模式主要特点就是保证对象只被实例化一次,所以构造方法的私有化,才能保证不能随便的去new() 对象,从而保证对象只能初始化一次。

2.为什么成员变量要用 static 修饰?

程序调用类中方法只有两种方式,①创建类的一个对象,用该对象去调用类中方法;②使用类名直接调用类中方法,格式“类名.方法名()”;
现在没有办法new 对象了,所以只能使用第二种方式。
java中静态方法没有办法调用非静态的类或者变量,所以成员变量也需要使用static来修饰。

3.单例模式的应用场景?

  • 数据库连接池:数据库连接池是一个重要的资源,单例模式可以确保应用程序中只有一个数据库连接池实例,避免资源浪费。
  • 配置文件管理器:应用程序通常需要一个配置文件管理器来管理配置文件,单例模式可以确保在整个应用程序中只有一个这样的实例。
  • 缓存系统:缓存系统是提高应用程序性能的重要组件,单例模式可以确保只有一个缓存实例。

4.单例模式使用的注意情况

单例模式主要分为 懒汉 和饿汉,我们通常再使用的时候要综合评估两种方式的优缺点,决定使用,比如:对于一些占用内存小的类我们使用饿汉模式,占用内存较大的类我们就使用懒汉模式。一开始就需要加载的并且会被频繁使用的就用饿汉模式。

5.JDK中的单例

java.lang.Runtime类使用的就是单例模式(饿汉),这个类是运行时的类,很多信息需要获取所以使用的是饿汉单例模式,如下:
在这里插入图片描述
java.awt.Desktop类使用的是懒汉单例模式:
在这里插入图片描述

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

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

相关文章

验证码生成--kaptcha

验证码生成与点击重新获取验证码 如图所示&#xff0c;本文档仅展示了验证码的生成和刷新显示。 1. 概述 系统通过生成随机验证码图像和文本。 2. 代码分析 2.1. Maven依赖 <dependency><groupId>com.github.penggle</groupId><artifactId>kaptch…

VirtualBox7安装ubantu server 22.04通过NAT+Only-Host双网卡实现宿主机与虚拟机互通

目录 背景环境安装虚拟机配置网卡修改ssh端口遇到的坑参考文章 背景 时间长没用docker了&#xff0c;有些命令都快忘了&#xff0c;心血来潮想着搞个docker玩一玩&#xff0c;所以需要先搞一个虚拟机&#xff0c;因为之前CentOS用的比较多&#xff0c;所以这次想试一试ubantu。…

Java入门——继承和多态(上)

包 包是组织类的一种方式. 使用包的主要目的是保证类的唯一性. 例如, 你在代码中写了一个 Test 类. 然后你的舍友也可能写一个 Test 类. 如果出现两个同名的类, 就会冲突, 导致 代码不能编译通过. 导入包中的类 Java 中已经提供了很多现成的类供我们使用. 例如 public cla…

【C -> Cpp】由C迈向Cpp (5)

标题&#xff1a;【C -> Cpp】由C迈向Cpp&#xff08;5&#xff09; 水墨不写bug &#xff08;图片来源于网络&#xff09; 不抵制失败&#xff0c;携手失败&#xff0c;迈向成功 正文开始&#xff1a; &#xff08;一&#xff09;深入理解构造函数 在之前的讲解中&#x…

linux系统(ubuntu)调用科大讯飞SDK实现语音识别

1. 科大讯飞官网 登录注册实名制 2. 点击控制台&#xff0c;创建应用 点击左侧的语音听写&#xff0c;右边下滑选择Linux&#xff0c;点击下载 选择Linux平台&#xff0c;普通版本&#xff0c;语音听写&#xff0c;SDK下载 此时将得到一个压缩包&#xff0c;选择的功能不…

LVS的三种工作模式---(DR/TUN/NAT)

目录 一、NAT模式&#xff08;LVS-NAT&#xff09; 二、IP隧道模式&#xff08;LVS-TUN&#xff09; 三、DR模型--直接路由模式&#xff08;LVS-DR&#xff09; LVS/DR模式ARP抑制 原因&#xff1a; LVS的DR工作模式及配置&#xff1a; LVS的NAT工作模式及配置&#xff1…

邂逅Linux--常见指令,万物为文件(一)

引子&#xff1a;在之前&#xff0c;我们经常听到Linux&#xff0c;那什么是Linux呢&#xff1f;Linux是一种免费使用和自由传播的类UNIX操作系统&#xff0c;其内核由林纳斯本纳第克特托瓦兹&#xff08;Linus Benedict Torvalds&#xff09;于1991年10月5日首次发布&#xff…

命令行工具部署达梦数据库 DMDPC(BP 多副本架构)

解达梦数据库DPC集群的主要使用场景&#xff1a; DMDPC 关注和解决的是大数据、计算与存储分离、高可用、支持全部的 SQL 标准、拥有完整的事务处理能力和集群规模能够动态伸缩的业务场景&#xff1a; 大量的复杂查询操作要求优化器能够生成优良的执行计划&#xff0c;并且执…

0基础理解ECC并做题-攻防世界easy-ECC理解

基点p就是最初选定的那个点 1和2都是整数集合&#xff0c;但是1/20.5就不属于整数集合 一直加&#xff0c;一直乘&#xff0c;还能保证有限个数字&#xff1f;这是因为采用了取模的运算&#xff0c;让元素始终都在有限的范围内。 如何计算分数求模&#xff1f; 设n1/2mod23,那么…

使用注解的方式进行配置RabbitMQ

引入依赖&#xff1a; <dependency><groupId>org.springframework.amqp</groupId><artifactId>spring-rabbit-test</artifactId><scope>test</scope></dependency> 配置application.yml server:port: 8082 spring:rabbitmq…

Coze扣子开发指南:AI零代码编程创建插件

在Coze扣子中创建插件&#xff0c;有两种方式&#xff0c;一是用API&#xff0c;具体方式参照上一篇文章《Coze扣子开发指南&#xff1a;用免费API自己创建插件》&#xff0c;还有一种方式就是编程&#xff0c;不过有了AI的帮助&#xff0c;即使不会编程的人&#xff0c;也可以…

如何清除DNS缓存,刷新DNS

大家在使用域名访问服务器的时候&#xff0c;经常会遇到一个问题&#xff0c;同一个局域网里的两台电脑&#xff0c;一台可以访问而另一台不行。这是为什么呢&#xff1f;这里我要和大家说下DNS缓存的问题&#xff0c;顾名思义&#xff0c;每台电脑都有DNS缓存&#xff0c;在域…

MyBatis-plus(一):快速入门

目录 一、MyBatis-plus 快速入门 1、原理 2、实体类命名规则 3、常见注解 4、主键 id 策略 5、使用 TableField 的常见场景 6、常用配置 二、核心功能 1、条件构造器 2、自定义 SQL 3、IService 接口 一、MyBatis-plus 快速入门 1、原理 MyBatisPlus 通过扫描实体…

Leetcode 剑指 Offer II 077.排序链表

题目难度: 中等 原题链接 今天继续更新 Leetcode 的剑指 Offer&#xff08;专项突击版&#xff09;系列, 大家在公众号 算法精选 里回复 剑指offer2 就能看到该系列当前连载的所有文章了, 记得关注哦~ 题目描述 给定链表的头结点 head &#xff0c;请将其按 升序 排列并返回 排…

STM32入门_江协科技_5~6_OB记录的自学笔记_GPIO输出_LED流水灯_蜂鸣器

5. GPIO 输出 5.1. GPIO简介 GPIO&#xff08;General Purpose Input Output&#xff09;通用输入输出口可配置为8种输入输出模式引脚电平&#xff1a;0V~3.3V&#xff0c;部分引脚可容忍5V&#xff08;端口输入5V的电压&#xff0c;之前引脚定义表格中带FT标识的&#xff09…

暗区突围哪里获得测试资格 暗区突围测试资格获取方法

在游戏业界的浩瀚星空中&#xff0c;《暗区突围》如同一颗璀璨新星&#xff0c;以其独树一帜的游戏模式和前所未有的沉浸式体验&#xff0c;迅速吸引了全球玩家的目光。它不仅仅是一款游戏&#xff0c;更像是一次对勇气、智慧与团队合作的深度探索。玩家在危机四伏的暗区中&…

【软考高项】四十四、高级项目管理

一、项目集管理 相关角色 项目集发起人、项目集指导委员会、项目集经理、其他影响项目集的干系人项目集管理绩效域 项目集战略一致性、项目集效益管理、项目集干系人参与、项目集治理和项目集生命周期管理 二、项目组合管理 项目组合经理角色 项目组合管理原…

软件测试报告(交付文档支撑word原件)

软件测试报告在软件开发过程中起着至关重要的作用&#xff0c;主要有以下几个主要原因&#xff1a; 1、确保软件质量 2、提供决策支持 3、记录测试过程和结果 4、促进沟通和协作 5、符合标准和法规要求 6、改进测试流程和策略 7、降低风险 软件开发全套资料获取进主页或者本文…

【字符函数与字符串函数】

文章目录 一、strlen函数1.strlen函数的使用2.strlen函数的模拟实现(1)计算器办法(2)不创建临时变量计数器(3)指针 二、strcpy函数1、strcpy函数的使用2、strcpy函数的模拟实现 三、strcat函数1、strcat函数的使用2、strcat模拟实现3、字符串自己给自己追加&#xff1f; 四、st…

2024年数维杯数学建模

高质量原创论文已完成 需要的私我