单例模式的4种写法

2023-11-06

单例模式是开发过程中常用的模式之一,首先了解下单例模式的四大原则:

  1. 构造方法私有;
  2. 以静态方法或枚举返回实例;
  3. 确保实例只有一个,尤其是多线程环境;
  4. 确保反射或反序列化时不会重新构建对象;

饿汉模式

饿汉模式在类被初始化时就创建对象,以空间换时间,故不存在线程安全问题。

public class SingleTon{
    // 创建对象
   	private static SingleTon INSTANCE = new SingleTon();
    // 构造方法私有
 	private SingleTon(){}
    // 返回实例
	public static SingleTon getInstance(){ return INSTANCE; }
}

饱汉模式

饱汉是变种最多的单例模式。我们从饱汉出发,通过其变种逐渐了解实现单例模式时需要关注的问题。

基础饱汉

public class Singleton {
  	  
    private static Singleton singleton = null;
    
    private Singleton() {}
    
  	public static Singleton getInstance() {
    	if (singleton == null) {
      		singleton = new Singleton();
    	}
    	return singleton;
  	}
}

饱汉模式的核心就是懒加载。好处是更启动速度快、节省资源,一直到实例被第一次访问,才需要初始化单例;缺点是线程不安全,if语句存在竞态条件。

DCL(Double Check Lock)

上面说到基础饱汉模式的缺点是线程不安全,在多线程环境下无法保证实例唯一,因此只需要在关键语句上加锁即可解决问题:

public class Singleton {
  	  
    private static Singleton singleton = null;
    
    private Singleton() {}
    
  	public synchronized static Singleton getInstance() {
    	if (singleton == null) {
      		singleton = new Singleton();
    	}
    	return singleton;
  	}
}

第一种解决方式简单粗暴,直接在获取实例的方法上加锁,保证了多线程情况下实例唯一,但是synchronized操作是很耗时的,导致的结果就是并发性能极差,因此可对其进行修改:

public class Singleton {
  	  
    private static Singleton singleton = null;
    
    private Singleton() {}
    
  	public static Singleton getInstance() {
    	if (singleton == null) {
      		synchronized(Singleton.class){
                if (singleton == null) {
      				singleton = new Singleton();
    			}
            }
    	}
    	return singleton;
  	}
}

这就是双重检查锁模式(DCL,Double Check Lock),在这种模式下,基本达到了理想的效果(懒加载+线程安全);

事实上,DCL模式仍存在一些问题,第11行中singleton = new Singleton()并不是一个原子操作,它在jvm中分为3步执行:

  1. memory = allocate(); //在堆内存开辟内存空间;
  2. initInstance(memory); // 在堆内存中实例化SingleTon里面的各个参数;
  3. instance = memory; //把对象指向堆内存空间。

由于jvm指令重排的机制,第3步操作是可能被优化到第2步操作之前的,此时若有新的线程进入该方法,经if判断后(因第3步操作使该对象指向了一块内存空间,所以if判断结果为false)会直接返回singleton,即引用instance指向内存memory时,该内存还未被初始化;这就是著名的DCL失效问题,解决方法也很简单,使用volatile关键字修饰即可;

Holder模式

我们既希望利用饿汉模式中静态变量的方便和线程安全;又希望通过懒加载规避资源浪费。Holder模式满足了这两点要求:核心仍然是静态变量,足够方便和线程安全;通过静态的Holder类持有真正实例,间接实现了懒加载。

public class Singleton {
    
    private static class SingletonHolder {
    	private static final Singleton singleton = new Singleton();
    	private SingletonHolder() {
    	}
  	}
    
  	private Singleton() {}

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

静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。即当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载SingleTonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

虚拟机会保证一个类的在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的方法,其他线程都需要阻塞等待,直到活动线程执行初始化方法完毕。

枚举模式

public enum Singleton {
  SINGLETON;
}

枚举在java中与普通类一样,都能拥有字段与方法,而且枚举实例创建是线程安全的,在任何情况下,它都是一个单例;但是缺点是可读性极差。
通过反编译我们可以看到枚举类的本质:
在这里插入图片描述

本质上和饿汉模式相同,区别仅在于公有的静态成员变量。

反射与反序列化攻击

以上介绍的几种模式,除了枚举模式外,其他模式均无法防止反射攻击和反序列化攻击;

反射演示:

public static void main(String[] args) throws Exception{
    SingleTon singleTon = SingleTon.getInstance();

    Constructor<SingleTon> constructor = SingleTon.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    SingleTon singleTon1 = constructor.newInstance();
    System.out.println(singleTon1 == singleTon);
    // 结果为false
}

解决办法:反射是通过构造方法构建实例的,那么只需要在构造方法中加入判空即可:
在这里插入图片描述

反序列化演示(需单例类实现Serializable接口):

public static void main(String[] args) throws Exception{
    SingleTon singleTon = SingleTon.getInstance
    FileOutputStream out = new FileOutputStream("SingleTon.txt");
    ObjectOutputStream oos = new ObjectOutputStream(out);
    oos.writeObject(singleTon);
    oos.close();
    out.close
    FileInputStream in = new FileInputStream("SingleTon.txt");
    ObjectInputStream obj = new ObjectInputStream(in);
    SingleTon singleTon1 = (SingleTon) obj.readObject();
    in.close();
    obj.close();
    System.out.println(singleTon == singleTon1);
    // 输出结果为false
}

解决办法:定义readResolve()方法,反序列化时,如果定义了readResolve()则直接返回此方法指定的对象。而不需要单独再创建新对象。
在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

单例模式的4种写法 的相关文章

  • 在java中将StreamWriter转换为OutputStream?

    我正在尝试使用 System setOut 将 System out 重定向到字符串 它需要一个 PrintStream 有什么方法可以将 StringWriter 转换为 Stream 以便我可以将其传递给 setOut 吗 你不能完全这
  • Android NumberPicker 带字符串

    I have customised the NumberPicker to show text The output is this 当我按 确定 时 我想将 e x 鼠标添加到我的列表 文章 中 我得到的是索引值 int 它由 array
  • JAVA - 带有特殊字符的 LDAP 密码不起作用

    我试图在我的系统上创建一个登录屏幕 在 Active Directory 中进行查询 但是当用户的密码包含一些特殊字符 如 和 时 它不会验证 我需要加密密码才能工作吗 我该怎么做 我使用 getPassword 通过 JPasswordF
  • 如何使用 SLF4J 和 Log4j2 记录 FATAL(或任何自定义日志级别)

    我有那些具体的要求 需要能够登录FATAL level 需要使用SLF4J 需要使用Log4j2 现在 这是我的执行 final Logger logger LoggerFactory getLogger HelloWorld class
  • 以编程方式将 PEM 证书导入 Java KeyStore

    我有一个由两个文件 crt 和 key 组成的客户端证书 我希望将其导入到 java KeyStore 中 然后在 SSLContext 中使用 以通过 Apache 的 HTTPClient 发送 HTTP 请求 但是 我似乎找不到一种以
  • 如何在Spring的applicationContext.xml中指定默认范围来请求范围?

    我想让所有 bean 请求默认作用域 但是 Spring 文档说默认作用域是 Singleton 第 3 4 1 和 3 4 2 节http static springsource org spring docs 2 5 x referen
  • JUnit Eclipse 显示 System.out.print() 的

    我正在使用 JUnit 3 和 Eclipse 3 4 当我运行 JUnit 测试用例时 一切正常并且测试完美完成 唯一的事情是我想查看我正在运行的类的输出 所有类都具有一些输出值的基本 System out print 因此 当我运行测试
  • Maven 多模块项目结构问题

    自从过去几周构建我的 Maven 多模块项目以来 这是我的一次有趣的经历 当我决定使用 Maven 进行构建生命周期管理时 我有几个原因希望选择 Maven A 大多数开发团队都是分开的 这样每个团队都可以在项目中的单独模块上工作 例如团队
  • 如何自定义JProgressBar?

    我正在制作一个启动器 我想要一个自定义的进度栏 我已经做了一些研究 并且可以使用 JavaFX 从未用它做过任何事情 并且可以通过替换 UI 来实现 我正在寻找一个具有圆形边缘和圆形填充的酒吧 像这样的事情 package gui impo
  • Java String.format 向整数添加空格

    我有一小段代码 我不明白输出 此输出向我的字符串格式文本添加空格 我做错了什么吗 public class HelloWorld public static void main String args int a1 540 int a2 4
  • 使用外部硬盘写入和存储 mysql 数据库

    我已经设置了 mysql 数据库在我的 Mac 上使用 java 和 eclipse 运行 它运行得很好 但现在我将生成大约 43 亿行数据 这将占用大约 64GB 的数据 我存储了大量的密钥和加密值 我有一个 1TB 外部我想用作存储位置
  • 字节码和位码有什么区别[重复]

    这个问题在这里已经有答案了 可能的重复 LLVM 和 java 字节码有什么区别 https stackoverflow com questions 454720 what are the differences between llvm
  • RMI 服务器:rmiregistry 或 LocateRegistry.createRegistry

    对于服务器端的RMI 我们需要启动吗rmiregistry程序 或者只是调用LocateRegistry createRegistry 如果两者都可以的话 各有什么优点和缺点 他们是同一件事 rmiregistry是一个单独的程序 您可以从
  • Scala repl 抛出错误

    当我打字时scala在终端上启动 repl 它会抛出此错误 scala gt init error error while loading AnnotatedElement class file usr lib jvm java 8 ora
  • Android同步onSensorChanged?

    这是我的问题的后续 Android线程可运行性能 https stackoverflow com questions 36395440 android thread runnable performance 我在理解应用程序的同步方法时遇到
  • Java泛型类型

    当我有一个界面时 public interface Foo
  • 监控 Java 应用程序上的锁争用

    我正在尝试创建一个小基准 在 Groovy 中 以显示几个同步方法上的高线程争用 当监控自愿上下文切换时 应该会出现高争用 在 Linux 中 这可以通过 pidstat 来实现 程序如下 class Res private int n s
  • 日期时间解析异常

    解析日期时 我的代码中不断出现异常错误 日期看起来像这样 Wed May 21 00 00 00 EDT 2008 这是尝试读取它的代码 DateTimeFormatter formatter DateTimeFormatter ofPat
  • Java 9 中紧凑字符串和压缩字符串的区别

    有什么优点紧凑的字符串 http openjdk java net jeps 254JDK9 中的压缩字符串 压缩字符串 Java 6 和紧凑字符串 Java 9 都有相同的动机 字符串通常实际上是 Latin 1 因此浪费了一半的空间 和
  • 如何使用 Spring AOP 建议静态方法?

    在执行类的静态方法之前和之后需要完成一些日志记录 我尝试使用 Spring AOP 来实现这一点 但它不起作用 而对于正常方法来说它起作用 请帮助我理解如何实现这一点 如果可以使用注释来完成 那就太好了 也许您应该在使用 Spring AO

随机推荐

  • 打开xshell无法定位程序输入点。。。。。。。。。。于动态链接库nssock2.dll上解决方法(参考)

    下载好Xshell和Xftp后 注意版本一致 打开Xshell 第一次可能没问题 第二次进入会报错 打开xshell无法定位程序输入点 于动态链接库nssock2 dll上 或者点击上传文件的时候 明明安装好Xftp 但是还是报该错误 尝试
  • Linux环境下一些配置记录

    Cmake出现The CXX Compiler not found错误 sudo apt get update sudo apt get install build essential sudo apt get install libgle
  • Flutter 使用Positioned 时异常BoxConstraints forces an infinite width.

    The following assertion was thrown during performLayout BoxConstraints forces an infinite width These invalid constraint
  • 记录实现el-table列表的无限滚动(InfiniteScroll)--使用原生Scroll

    由于接收到要求 项目由vue2切换成vue3 要前台列表使用无限滚动展示数据 我在查阅资料后发现原来官方推荐的方法是vue infinite scroll这个现在已经不在维护的插件 虽然vue infinite scroll确实挺好的 但就
  • Git 右键添加Git Bash Here

    小Z由于不知原因 右键没有了Git Bash Here 没有这个右键菜单导致获取Git仓库中的代码很不方便 所以决定通过注册表的方式将这个菜单加出来 运行regedit exe进入注册表 在HKEY CLASSES ROOT Directo
  • 用例图分析---学生成绩管理系统

    尝试画出学生成绩管理的用例图 用例有 登录 找回密码 输入 修改 保存 查询 删除成绩 参与者有教师与学生
  • JIS-CTF_VulnUpload靶机攻略

    本文中涉及到的相关漏洞已报送厂商并得到修复 本文仅限技术研究与讨论 严禁用于非法用途 否则产生的一切后果自行承担 vulnhub 是我喜爱的游乐场之一 上面的每个靶机都是很酷的一个游戏 完整找出所有 flag 只是基本任务 实现提权才是终极
  • 区块链应用 - LV的奢侈品验证平台

    领先的以太坊解决方案商ConsenSys与微软以及奢侈品牌路易威登合作开发了一个基于区块链的奢侈品验证平台 消费者可以使用该品牌验证买到的路易威登或迪奥产品 同属于LVMH 是否正品 该系统被称为 Aura 其设计目标是 服务于整个奢侈品行
  • 成长之路-- 从0开始学python(17)--Excel结合测试用例使用&数据驱动

    一般自动化测试的流程 准备测试数据 根据被测函数的参数 调用被测函数 得到实际结果 断言 这样的方法可以完成测试 但是存在不少的缺点 一个用例需要单独编写一个测试用例函数 方法 存在重复性代码 可以通过ddt解决 测试的数据都放在用例的方法
  • 对象成员的引用

    访问对象中的成员可以有3种方法 通过对象名和对象成员引用运算符 访问对象中的成员 通过指向对象的指针和指针成员引用运算符 gt 访问对象中的成员 通过对象的引用变量和对象成员引用运算符 访问对象中的成员 1 通过对象名访问对象中的成员 访问
  • 乳腺仿体breast phantom的MATLAB实现及探讨

    文献出处 2006 Development of an Analytic Breast Phantom for Quantitative Comparison of Reconstruction Algorithms for DBT I R
  • 一篇文章搞懂CMake(gcc、g++、cmake解释)

    一篇文章搞懂CMake gcc g cmake解释 这里写目录标题 一篇文章搞懂CMake gcc g cmake解释 gcc g cmake 1 CMake 流程 如何使用cmake 简单的CMake txt文件 参考 gcc gcc命令
  • C++14尝鲜:函数返回类型自动推导

    函数返回类型自动推导 函数返回类型自动推导是指C 11以及C 14中不直接给出函数返回类型 而是使用类型指示符来指示返回类型甚至彻底省略返回类型并最终由编译器来推导返回类型的语言特性 函数返回类型自动推导原则如下 当函数体内不包含任何返回值
  • 【译】Rust 中的错误处理

    Error Handling in Rust 译文 Rust 中的错误处理 原文链接 Error Handling in Rust Andrew Gallant s Blog 原文作者 Andrew Gallant 译文来自 GitHub
  • react配置之绝对路径

    绝对路径不多bb 简单的说就是 我们不再使用 之类的来表示路径位置了 直接上代码 我用的是react crate app脚手架 npm install react create app g react create app my react
  • advisor 2002的安装

    一 安装资源 http pan baidu com s 1sljurHN 二 遇到安装问题的解决方案 来源于博客 http blog csdn net qq 33565051 article details 52065063 1 在上面的网
  • 小程序首次授权拒绝后如何再次授权?(从用户的层面解决问题)

    写在前面 为了能及时的将自己踩到的前端坑 包括ionic angular react ReactNative 小程序 APICloud 分享给大家 以后会逐渐将文章转移到微信公众号 前端e家 front e family 可直接扫码关注 公
  • Java的常用日志技术(二)Logback详解

    之前我们学习了日志实现框架JUL Log4j 以及 Log4j 各种配置方式 此章我们将在 log4j 的配置文件格式的基础上 学习 logback 及相关配置 Logback简介 Logback是由log4j 创始人设计的又一个开源日志组
  • vscode 个人使用配置

    因公司绿盾加密导致使用source insight远程访问代码受限 转战vscode 把自己第一次接触以来的操作记录一下 省的哪天要重新配置还得百度半天 文章目录 1 插件安装 2 设置中文 3 设置ssh远程访问代码 3 1 安装Remo
  • 单例模式的4种写法

    单例模式是开发过程中常用的模式之一 首先了解下单例模式的四大原则 构造方法私有 以静态方法或枚举返回实例 确保实例只有一个 尤其是多线程环境 确保反射或反序列化时不会重新构建对象 饿汉模式 饿汉模式在类被初始化时就创建对象 以空间换时间 故