设计模式-单例模式

面向对象语言讲究的是万物皆对象。通常流程是先定义可实例化类,然后再通过各种不同的方式创建对象,因此类一般可以实例化出多个对象。但是实际项目开发时,我们还是希望保证项目运行时有且仅包含一个实例对象。这个需求场景的出发点包括但不限于以下几个方面:数据源对象(创建连接池、权限验证等均十分消耗资源)、线程池对象(同一个线程池)、日志对象(防止日志覆盖)等等。

一、单例模式概念

单例模式(Singleton Pattern)是指在一个系统应用中只有一个实例,并且是自行实例化的。其完整表述为:

Ensure a class has only one instance, and provide a global point of access to it.

Java单例模式是一种广泛使用的设计模式,单例模式有很多好处,他能够避免实例对象的重复创建,不仅可以减少每次创建对象的时间开销,还可以节约内存空间;

二、解决方案

为解决项目中有且仅存在唯一对象的需求,下面给出常见的几种单例模式的实现方案,并分析其优缺点。

2.1 饿汉式单例

饿汉式单例是指在项目启动加载类时进行初始化单例对象,并提供向外暴露单例对象的方法。代码如下:

/*** 饿汉式单例模式*/
public class HungrySingleton {// 类初始化时对象实例化private static HungrySingleton hungrySingleton = new HungrySingleton();// 构造器私有化private HungrySingleton() {}// 向外暴露获取单例对象的方法public static HungrySingleton getInstance() {return hungrySingleton;}
}

在这里插入图片描述
饿汉式单例模式特点:

  1. 单例类构造器私有化。(防止其他地方通过构造器创建对象)
  2. 单例对象初始化时机为类加载,并且是私有类成员变量
  3. 提供向外暴露单例对象方法

饿汉式单例模式优点:

  • 最简单的单例模式,执行效率最高
  • 线程安全(类加载时初始化)

虽然如此,但需要注意到,由于单例对象在类加载时即初始化完成,因此有可能此时初始化的对象并不会被业务逻辑使用,造成内存的浪费,而且无法被GC回收,造成内存利用率低。
因此,这种饿汉式单例模式的应用场景一般是单例对象被业务逻辑强依赖,即单例对象会被频繁的使用。

2.2 懒汉式单例

为解决饿汉式单例的对象浪费内存空间问题,对于不会被频繁使用的单例对象可以考虑使用懒汉式单例模式来初始化。懒汉式单例模式就是将对象初始化时机延迟到请求对象中,如果单例对象已经存在,直接返回,如果不存在单例对象,再进行初始化。初步代码如下:

/*** 懒汉式单例模式-非线程安全*/
public class LazySimpleSingleton {private static LazySimpleSingleton instance;private LazySimpleSingleton() {}// 向外暴露获取单例对象的方法public static LazySimpleSingleton getInstance() {if (instance == null) {     // 如果单例对象为null,则初始化单例对象instance = new LazySimpleSingleton();}return instance;}
}

类图和饿汉式基本无变化,区别仅在于单例对象的初始化时机。然而,饿汉式单例由于初始化是在类加载时进行,JVM已经保证了在多线程下只会被执行一次加载逻辑,但是懒汉式此例中无法保证在多线程下是否会被初始化多次,即产生了多个对象。
因此,获取单例对象时必须考虑线程安全问题。线程安全我们可以使用JVM提供的synchronized关键字修饰getInstance()保证获取实例对象方法时线程安全的,但是,这种方案也是存在问题,在高并发下,大量的请求同时需要获取单例对象时,就会由于同步锁竞争的原因使得服务性能变差。那还有没有其他方案既能保证线程安全,也能保证性能不受影响呢?答案是双重检查锁(Double Check Lock, DCL)。
双重检查锁大概描述为,第一重检查单例对象是否存在,存在就返回,不存在则进入synchronized同步代码块,在锁内部并进行第二重检查单例对象是否存在,存在即返回,不存在则初始化单例对象。这里面,首先第一重检查保证了大部分请求均不会收到锁的性能影响,第二重检查保证单例对象只会被初始化一次。实际代码如下:

/*** 懒汉式单例模式-双重检查锁*/
public class LazyDoubleCheckSingleton {private volatile static LazyDoubleCheckSingleton instance;private LazyDoubleCheckSingleton() {}public static LazyDoubleCheckSingleton getInstance() {if (instance == null) {     // 第一重检查:保证性能不受锁的影响synchronized (LazyDoubleCheckSingleton.class){  // 不存在单例对象时,才进入同步区if(instance == null) {      // 第二重检查:进入同步区再次检查保证确实不存在单例对象instance = new LazyDoubleCheckSingleton();}}}return instance;}
}

这种双重检查锁的单例实现方式就是既能够保证性能也能够保证延迟初始化对象的唯一性。需要说明的一点,你或许注意到了单例对象使用volatile关键字修饰,这是为什么呢?
首先volatile关键字有两个主要作用① 保证共享变量的可见性;② 禁止指令重排。前一个作用其实还好,因为synchronized本身就能够保证内存可见性,因此主要是由于第二个作用相关原因。继续我们再看new创建一个对象的实际过程主要分为三步:

  1. 分配对象内存空间
  2. 初始化对象
  3. 设置类成员变量执行分配的内存地址

cpu为了优化程序执行,可能会优化指令的执行顺序,如有可能第3步在第2步之前执行。如果线程A先执行了第3步,而没有执行第2步,此时CPU时间片轮转到线程B上,线程B会在第一次检查判断后返回单例对象,由于这里返回的对象实际并没有初始化完成,可能会导致线程B出现空指针或其他异常。
因此,使用volatile关键字修饰单例对象禁止CPU指令重拍优化,保证不会出现不成熟单例对象的出现和误用。

双重检查锁单例模式特点:

  1. 单例类构造器私有化。(防止其他地方通过构造器创建对象)
  2. 单例对象初始化时机为双重检查锁满足之后,并且是私有类成员变量,使用volatile修饰。
  3. 提供向外暴露单例对象方法

双重检查锁单例模式特点:

  • 对象懒加载,增加内存使用效率
  • 线程安全(双重检查锁+volatile关键字)

2.3 静态内部类单例

前面在使用双重检查锁的方式实现单例模式时,我们既要考虑性能、又要考虑线程安全,还需要考虑CPU指令重排的问题,实现起来相对十分复杂。并且另外一方面,synchronized同步锁在流量初期可能会出现性能尖刺问题,常常可能出现于线上发布器,也给系统稳定性带来了一定影响。有没有一种其他更好的方案呢?静态内部类单例可以算是一个答案。
如同饿汉式一样,静态内部类单例模式也还是利用JVM加载类能够保证线程安全的特性,但是又不能在单例类加载时就初始化单例对象,还是想这用到的时候才去初始化。解决这个问题的方法就是增加静态内部类,单例对象及其初始化维护在静态内部类中。而静态内部类的加载时机由getInstance()方法所决定。因此,静态内部类单例代码如下:

/*** 静态内部类单例模式*/
public class LazyStaticInnerClassSingleton {private LazyStaticInnerClassSingleton() {}public static LazyStaticInnerClassSingleton getInstance() {return LazyHolder.INSTANCE;     // 这里才会加载静态内部类LazyHolder}private static class LazyHolder {// 加载时会初始化单例对象private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();}
}

JVM将推迟LazyHolder的初始化操作,直到开始使用这个类时才初始化【因此,这也属于懒加载单例模式】,并且由于通过一个静态初始化来初始化单例对象,因此不需要额外的同步。当任何一个线程第一次调用getInstance时,都会使LazyHolder 被加载和被初始化,此时静态初始化器将执行单例对象的初始化操作。
静态内部类单例模式特点:

  1. 单例类构造器私有化。(防止其他地方通过构造器创建对象)
  2. 单例对象初始化时机为静态内部类加载
  3. 提供向外暴露单例对象方法

静态内部类单例模式特点:

  • 对象懒加载,增加内存使用效率
  • 线程安全(JVM保证)

2.4 枚举单例

枚举单例是《Effective Java》作者Joshua Bloch推荐使用的方式。以往的单例模式都有如下3个特点:

  1. 构造方法私有化
  2. 实例化的变量引用私有化
  3. 获取实例的方法共有

但是这种实现方式的问题就在于“私有化构造器并不保险”,因为私有构造方法仍然可以通过反射获取另外一个实例,继而破坏了单例模式。为了能保证在反射、序列化场景下创建对象的唯一性,因此我们可以借助枚举来实现单例模式【为什么枚举不能被序列化和反序列化?详见下一篇文章】。

public enum EnumSingleton {INSTANCE;public static EnumSingleton getInstance() {return INSTANCE;}
}

枚举单例模式实际上也类似于静态内部类单例,枚举单例模式属于饿汉式单例,内部枚举被JVM加载时会初始化单例对象。之所以使用枚举来加载单例对象,就是因为枚举能够防止用户通过反射、序列化等方式破坏对象的唯一性。
单例模式特点:

  1. 使用枚举对象作为单例对象
  2. 提供向外暴露单例对象方法

枚举单例模式特点:

  • 饿汉式加载
  • 线程安全(JVM保证)
  • 防止反射、序列化等破坏单例的场景

【参考资料】

  1. 一文带你搞定单例模式-知乎
  2. 单例模式详解-知乎(阿里巴巴)
  3. 请在1分钟内写一个线程安全的恶汉单例-JVM
  4. 为何用Enum枚举实现被认为是最好的方式

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Unity游戏源码分享-射击游戏Low Poly FPS Pack 3.2

Unity游戏源码分享-射击游戏Low Poly FPS Pack 3.2 项目地址:https://download.csdn.net/download/Highning0007/88057717

循环退出语句break、continue,有什么区别?

目录 一、break语句二、continue语句三、break、continue语句有什么区别? 一、break语句 在Java中,break语句用于终止当前循环或switch语句的执行,并跳出该结构。当break语句被执行时,程序将会跳出包含该break语句的最内层的循环…

前端学习记录~2023.7.15~CSS杂记 Day7

前言一、介绍 CSS 布局1、正常布局流2、display 属性3、弹性盒子(1)设置 display:flex(2)设置 flex 属性 4、Grid 布局(1)设置 display:grid(2)在网格内放置元…

杨辉三角 II

给定一个非负索引 rowIndex,返回「杨辉三角」的第 rowIndex 行。 在「杨辉三角」中,每个数是它左上方和右上方的数的和。 示例 1: 输入: rowIndex 3 输出: [1,3,3,1] 示例 2: 输入: rowIndex 0 输出: [1] 示例 3: 输入: rowIndex 1 输出: [1,1]…

C++中的“三重”

博文内容:重载、重定义(隐藏),重写(覆盖) 三重区别及联系 概念联系及区别1、作用域2、函数要求 概念 重载 函数名相同,函数的参数列表不同(包括参数个数和参数类型),至于返回类型可同可不同。 …

【ABAP】数据类型(八)「表类型」

💂作者简介: THUNDER王,一名热爱财税和SAP ABAP编程以及热爱分享的博主。目前于江西师范大学本科在读,同时任汉硕云(广东)科技有限公司ABAP开发顾问。在学习工作中,我通常使用偏后端的开发语言ABAP,SQL进行任务的完成,对SAP企业管理系统,SAP ABAP开发和数据库具有较…

h5最新mtgsig1.1成品

h5最新mtgsig1.1成品 千锤百炼,方得始终

印刷企业如何利用MES管理系统实现智能计划排产

在数字化时代,印刷企业面临着日益激烈的市场竞争和不断攀升的成本压力。为了提高生产效率和质量,印刷企业需要采用先进的生产管理系统。其中,MES生产管理系统已成为实现智能计划排产的重要工具。本文将探讨如何利用印刷MES管理系统实现印刷企…

「深度学习之优化算法」(十四)麻雀搜索算法

1. 麻雀搜索算法简介 (以下描述,均不是学术用语,仅供大家快乐的阅读)   麻雀搜索算法(sparrow search algorithm)是根据麻雀觅食并逃避捕食者的行为而提出的群智能优化算法。提出时间是2020年,相关的论文和研究还比较少,有可能还有一些正在发表中,受疫情影响需要论…

session 生命周期和经典案例-防止非法进入管理页面

文章目录 session 生命周期和Session 经典案例-防止非法进入管理页面session 生命周期Session 生命周期-说明代码演示说明 Session 的生命周期创建CreateSession2创建ReadSession2 解读Session 的生命周期代码示例创建DeleteSession Session 经典案例-防止非法进入管理页面需求…

【25】SCI易中期刊推荐——神经网络科学(中科院4区)

💖💖>>>加勒比海带,QQ2479200884<<<💖💖 🍀🍀>>>【YOLO魔法搭配&论文投稿咨询】<<<🍀🍀 ✨✨>>>学习交流 | 温澜潮生 | 合作共赢 | 共同进步<<<✨✨ 📚📚>>>人工智能 | 计算机视觉…

三菱PLC 控制灯一秒钟交替闪烁

三菱PLC中常用的特殊继电器&#xff1a; M8000 上电一直ON标志 M8002 上电导通一次 M8004 PLC出错 M8005 PLC备用电池电量低标志 M8011 10ms时钟脉冲 M8012 100ms时钟脉冲 M8013 1s时钟脉冲 M8014 1min时钟脉冲 M8034…