【设计模式】单例模式

2023-05-16

【设计模式】单例模式

1 为什么要用单例?

单例设计模式(Singleton Design Pattern):一个类只允许创建一个对象(或实例)。

1.1 处理资源访问冲突

例如,我们需要实现一个往文件中打印日志的日志类,定义方式如下:

public class Logger {
  
  private FileWriter writer;
  
  public Logger() {
    File file = new File("/Users/wangzheng/log.txt");
    writer = new FileWriter(file, true); //true表示追加写入
  }
  
  public void log(String message){ 
    writer.write(mesasge);
  }
}

在业务中,有两块功能需要使用 Logger 记录日志:

public class UserController {
  private Logger logger = new Logger();
  
  public void login(String username, String password) {
    // ...省略业务逻辑代码...
    logger.log(username + " logined!");
  }
}

public class OrderController {
  private Logger logger = new Logger();
  
  public void create(OrderVo order) {
  // ...省略业务逻辑代码... 
  logger.log("Created an order: " + order.toString());
  }
}

这样的代码是存在问题的!那就是 在 Web 容器的 Servlet 多线程环境下,如果两个 Servlet 线程同时分别执行 login()create() 两个函数,并且同时写日志到 log.txt 文件中,那就有可能存在日志信息互相覆盖的情况。

简单来说就是 竞争资源的访问冲突问题

在这种情况下,单例设计模式的解决方法:

  • 节省内存空间。不需要创建那么多 Logger 对象。
  • 节省系统文件句柄。
  • 可避免多线程情况下,竞争资源访问冲突问题。

将 Logger 设计成一个单例类,程序中只允许创建一个 Logger 对象,所有的线程共享使用的这一个 Logger 对象,共享一个 FileWriter 对象,而 FileWriter 本身是对象级别线程安全的,也就避免了多线程情况下写日志会互相覆盖的问题。代码如下:

public class Logger {
  
  private FileWriter writer;
  
  // 创建私有、静态、不可修改地址的单例对象
  private static final Logger instance = new Logger();
  
  // 不能提供公开的构造器
  private Logger() {
    File file = new File("/Users/wangzheng/log.txt");
    writer = new FileWriter(file, true); //true表示追加写入
  }
  
  // 只能通过暴露出的方法进行静态成员变量访问
  public static Logger getInstance(){
    return instance;
  }
  
  public void log(String message){ 
    writer.write(mesasge);
  }
}

随后,其他业务代码如果需要访问该资源,需要通过公开的方法进行访问。

public class UserController {
  
  public void login(String username, String password) {
    // ...省略业务逻辑代码...
    Logger.getInstance().log(username + " logined!");
  }
}

public class OrderController {
  
  public void create(OrderVo order) {
  // ...省略业务逻辑代码... 
  Logger.getInstance()..log("Created an order: " + order.toString());
  }
}

1.2 表示全局唯一类

从业务概念上,如果一个数据在系统中至应保留一份(全局唯一),那就比较适合设计为单例类。

比如,配置信息类。在系统中,我们只有一个配置文件,当配置文件被加载到内存之后,以对象的形式存在,也理所应当只有一份。

2 如何实现一个单例?

要实现一个单例,我们需要关注的点:

  • 构造函数需要是 private 的,防止外部随意 new 类对象
  • 考虑对象创建时的线程安全问题
  • 考虑是否支持延迟加载
  • 考虑 getInstance() 性能是否高(是否加锁)

接下来,我们实现五种单例实现。

2.1 饿汉单例

饿汉就是特别饿,想要 立刻 有吃的。

饿汉式的实现方式比较简单。在类加载的时候,instance 静态实例就已经创建并初始化好了,所以,instance 实例的创建过程是线程安全的。不过,这样的实现方式不支持延迟加载。

public class IdGenerator{
  private AtomocLong id = new AtomicLong(0);
  
  // 静态实例私有化
 	private static final IdGenerator instance = new IdGenerator();
  
  // 构造器私有化
  private IdGenerator(){}
  
  // 创建访问方法
  public static IdGenerator getInstance(){
    return instance;
  }
  
 	public long getId(){
    return id.incrementAndGet();
  }
}

采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。

如果实例占用资源多,按照 fail-fast 的设计原则(有问题及早暴露),那我们也希望在程序启动时就将这个实例初始化好。如果资源不够,就会在程序启动的时候触发报错(比如 Java 中的 PermGen Space OOM),我们可以立即去修复。这样也能避免在程序运行一段时间后,突然因为初始化这个实例占用资源过多,导致系统崩溃,影响系统的可用性

结论:饿汉式不支持延迟加载

2.2 懒汉单例

懒汉就是特别懒,要用的时候才创建,能拖就拖。

懒汉式相对于饿汉式的优势就是支持 延迟加载

public class IdGenerator{
  private AtomocLong id = new AtomicLong(0);
  
  // 静态实例私有化,但一开始不创建
 	private static final IdGenerator instance;
  
  // 构造器私有化
  private IdGenerator(){}
  
  // 创建访问方法,加锁是为了防止高并发时创建多个 instance 对象
  public static synchronized IdGenerator getInstance(){
    if(instance == null){
      // 能拖就拖,用的时候才创建
      instance = new IdGenerator();
    }
    return instance;
  }
  
 	public long getId(){
    return id.incrementAndGet();
  }
}

不过懒汉式的缺点也很明显,我们给 getInstance() 这个方法加了一把大锁(synchronzed),导致这个函数的并发度很低。量化一下的话,并发度是 1也就相当于串行操作了。而这个函数是在单例使用期间,一直会被调用。如果这个单例类偶尔会被用到,那这种实现方式还可以接受。但是,如果频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶颈,这种实现方式就不可取了。

结论:懒汉式不支持高并发

2.3 双重检测

双重检测,是一种既支持延迟加载、又支持高并发的单例实现方式

本质上对懒汉单例进行改进。因为加锁是为了防止创建多个实例,那么实际上 只需要在创建的时候加锁,访问的时候就不需要锁了

在这种实现方式中,只要 instance 被创建之后,即便再调用 getInstance() 函数也不会再进入到加锁逻辑中了。所以,这种实现方式解决了懒汉式并发度低的问题。具体的代码实现如下所示:

public class IdGenerator{
  private AtomocLong id = new AtomicLong(0);
  
  // 静态实例私有化,但一开始不创建
 	private static final IdGenerator instance;
  
  // 构造器私有化
  private IdGenerator(){}
  
  // 创建访问方法,由于存在两次检测,故称双重检测
  public static IdGenerator getInstance(){
    if(instance == null){
      // 创建类级别的锁,只在初始化实例时有效
      synchronized(IdGenerator.class){
        if(instance == null){
          // 能拖就拖,用的时候才创建
      	  instance = new IdGenerator();
        }
      }
    }
    return instance;
  }
  
 	public long getId(){
    return id.incrementAndGet();
  }
}

2.4 静态内部类

我们再来看一种比双重检测更加简单的实现方法,那就是利用 Java 的 静态内部类。它有点类似饿汉式,但又能做到了延迟加载。具体是怎么做到的呢?我们先来看它的代码实现。

public class IdGenerator{
  private AtomocLong id = new AtomicLong(0);
  
  // 构造器私有化
  private IdGenerator(){}
  
  // 静态内部类
  private static class SingletonHolder{
    private static final IdGenerator instance = new IdGenerator();
  }
  
  public static IdGenerator getInstance() {
    // 返回静态内部类中的 instance
    return SingletonHolder.instance;
  }
  
  public long getId(){
    return id.incrementAndGet();
  }
}

SingletonHolder 是一个静态内部类,当外部类 IdGenerator 被加载的时候,并不会创建 SingletonHolder 实例对象。只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。insance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。

通过静态初始化来初始化 IdGenerator 为什么不需要额外的同步?

在初始器中采用了特殊的方式来处理 静态域(或者在静态初始化代码块中初始化的值),并提供了额外的线程安全性保证。静态初始化器是由 JVM 在类的初始化阶段执行,即在类被加载后并且被线程使用之前。由于 JVM 将在初始化期间获得一个锁,并且每个线程都至少获取一次这个锁以确保这个类已经加载,因此在静态初始化期间,内存写入操作将自动对所有线程可见。因此无论是在被构造期间还是被引用时,静态初始化的对象都不需要显式的同步。然而,这个规则仅适用于在构造时的状态,如果对象是可变的,那么在读线程和写线程之间仍然需要通过同步来确保随后的修改操作是可见的,以及避免数据破坏。

2.5 枚举

这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。具体的代码如下所示:

public enum IdGenerator {
  INSTANCE; 
  private AtomicLong id = new AtomicLong(0);
  public long getId() {
    return id.incrementAndGet();
  }
}

很简洁的一种实现方式,提供了序列化机制,保证线程安全,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。

总结

方式优点缺点
饿汉线程安全、效率高不可延迟加载
懒汉线程安全、可延迟加载效率低
双重检测线程安全、可延迟加载、效率高
静态内部类线程安全、可延迟加载、效率高
枚举线程安全、效率高不可延迟加载
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【设计模式】单例模式 的相关文章

  • 采样频率变化时,滤波器的性能会变差吗?

    一 问题来源 飞控需要采集加速度计与陀螺仪数据进行低通滤波 xff0c 一般以1kHz频率采集 xff0c 即采样频率为1kHz 使用二阶巴特沃兹低通滤波器进行滤波 xff0c 截至频率可设置为20Hz 滤波器的输入参数一般是采样频率 截至
  • Carsim2019与Simulink (Matlab2018b)联合仿真成功(超详细)

    首先 xff0c 想吐槽一下 xff0c 一些教程和英文帮助文档一样 xff0c 逐个介绍界面按钮 xff0c 人都被转晕了也进入不了主题 终于 xff0c 看了UP主自动驾驶废材的 xff1a 从零开始搭建carsim与simulink联
  • 基于xr871机器人故事机设计原理和方法 图灵后台

    原创 xff1a http blog csdn net kylin fire zeng xff0c 欢迎转载分享 xff0c 共同进步 xff0c 但请注明出处啊 xff0c 尊重他人成果
  • GPS速度测量

    GPS使用M8N xff0c 放在地上一两分钟 xff0c 举起来 xff0c 放下 xff0c 举高走一走 xff0c 放下 可见 xff0c GPS可以测量NED系下各周速度 但是 z 轴速度收敛慢 xff0c 不是很准确 也可能GPS
  • GPS经纬度转距离

    function span class token punctuation span pN span class token punctuation span pE span class token punctuation span spa
  • STM32 移植Mavlink,Keil报错

    STM32 移植Mavlink xff0c Keil报一大堆错误 span class token keyword static span span class token keyword void span span class toke
  • Linux之串口接收响应试验

    问题来源 仪表作为从机时 xff0c 需要分析仪表的响应速度 但发现采用linux或windows开发时发现串口响应较慢 在网上没有查到相关的资料 xff0c 于是在手上的linux产品上进行试验 xff08 iMAX6Q xff09 试验
  • 用TCP模拟网页服务器开发记录

    一 所需要撑握的技术 xff1a 1 TCP 下的socket应用 2 多线程动态创建 xff0c 实现多个客户端的同时访问 3 了解html的语法结构和javaScript的开发 4 了解http数据包格式 包括json get post
  • 安装docker

    安装docker 参考网站 xff1a https docs docker com install linux docker ce ubuntu sudo apt get update sudo apt get install apt tr
  • 自动化控制行业常见面试问题分析

    一 系统建模 xff0c 二 经典控制与现代控制 xff0c 三 自动检测 xff0c 四 过程控制 xff0c 五 计算机控制 xff0c 六 微机原理 xff0c 七 电子技术综合 xff0c 八 电机控制 xff0c 九 专业英语名词
  • glibc 下载及编译

    glibc下载站点 http ftp gnu org pub gnu glibc 下载点 http www gnu org software libc bugs html 官网 uclibc下载站点 https downloads ucli
  • 树莓派镜像制作

    前言 本文主要讲诉如何制作一个树莓派的启动镜像 xff08 本人使用的是4b 43 xff09 镜像下载 镜像下载64bit xz文件大小在756M xff0c 解压后镜像3 95个G 上面的镜像下载比较慢 xff0c 可以使用国内的镜像源
  • 推荐算法评价指标(ACC、查全率、查准率、F1-Score、ROC、AUC,P-R)

    文章目录 混淆矩阵二级指标三级指标F1 ScoreROC AUC PR曲线 推荐系统的评价指标很多 xff0c 今天介绍 xff1a 准确率 ACC 查准率 P精确率 查全率 R召回率 F1 score AUC值 xff0c ROC曲线 P
  • 规划xr871实现儿童故事机的基本功能

    原创 xff1a http blog csdn net kylin fire zeng xff0c 欢迎转载分享 xff0c 共同进步 xff0c 但请注明出处啊 xff0c 尊重他人成果
  • Maven项目中pom文件的parent标签报错如何解决 (eclipse中创建springboot项目时,pom中的parent标签报错)

    原因 xff1a maven的settings文件不能从连网下载依赖jar包 解决办法 xff1a 在Maven修改settings文件内容 etting配置镜像仓库 span class token tag span class toke
  • Android中Paint字体的灵活使用

    在Android开发中 xff0c Paint是一个非常重要的绘图工具 xff0c 可以用于在控制台应用程序或Java GUI应用程序中绘制各种形状和图案 其中 xff0c Paint setText 方法是用于设置Paint绘制的文本内容
  • 像素类型

    不同的图像有不同的像素类型 xff0c 不过对于不同的像素类型 xff0c 需要在模板参数传入不同的值 首先像素的数据类型包括CV 32U xff0c CV 32S xff0c CV 32F xff0c CV 8U xff0c CV 8UC
  • 20220806 美团笔试五道编程题(附AK题解)

    恭喜发现宝藏 xff01 微信搜索公众号 TechGuide 回复公司名 xff0c 解锁更多新鲜好文和互联网大厂的笔经面经 作者 64 TechGuide 全网同名 点赞再看 xff0c 养成习惯 xff0c 您动动手指对原创作者意义非凡
  • Ubuntu入门(二) 用户权限 文件权限 磁盘管理 连接文件

    目录 1 用户权限管理1 1 用户系统1 2 创建用户和用户组命令1 3 权限管理1 4 权限管理命令 2 磁盘管理2 1 磁盘管理基本概念2 2 磁盘管理命令 3 连接文件3 1 硬连接3 2 符号连接 xff08 软连接 xff09 1
  • git clone指定分支代码

    全克隆 xff1a span class token function git span clone span class token operator lt span url span class token operator gt sp

随机推荐

  • Kali Linux 镜像 各个版本之间的区别

    按处理器架构来划分的话 xff0c 有 64 bit 32 bit armhf armel等版本 加light的是轻量版 e17 Mate LXDE等是不同的桌面环境 Kali Linux 2016 2 支持GNOME KDE Mate L
  • ubuntu16.04下px4环境搭建与固件编译

    px4官网提供了一个批处理方式搭建px4开发环境 xff0c 十分好用 xff0c 按照官网步骤 1 sudo usermod span class hljs a span G dialout span class hljs variabl
  • Top-down与Bottom-up

    简析 顾名思义 xff0c top down是由上至下 xff0c 而bottom up由下至上的意思 其实 xff0c 就我对本行业的理解 xff0c top 61 目的 objective xff0c bottom 61 方法细节 ac
  • MATLAB GPU加速

    以前使用matlab 的时候 xff0c 很多人都用过里面的并行工具箱 xff0c 用的最多的应该就是parfor 实际上 xff0c matlab里面已经有不少工具箱里面都有了支持GPU加速的函数 使用matlab 43 GPU加速的前提
  • 论如何最低成本进入 智能家居、人工智能、安卓主板 的领域

    原创 xff1a http blog csdn net kylin fire zeng xff0c 欢迎转载分享 xff0c 共同进步 xff0c 但请注明出处啊 xff0c 尊重他人成果
  • 为什么说枚举单例模式是最安全的?

    单例模式有很多种 xff0c 如 xff1a 饿汉式 线程安全 xff0c 但是浪费资源 xff1b 懒汉式 懒汉式又分为 xff0c 同步锁单例模式 性能较差 xff1b 双重判断同步锁单例模式 静态内部类单例模式 以上单例模式大多数都是
  • Qt - 信号与槽的连接方式

    信号与槽的连接方式主要有以下5种方式 xff1a 先看代码 xff1a ifndef MYOBJECT H define MYOBJECT H include lt QObject gt class MyObject public QObj
  • vslam流程框架介绍

    平常扫地机产品上经常听说使用了vslam技术 xff0c 那么这个vslam到底是什么呢 xff0c 我们下面一起来看看 vslam是什么 xff1f VSLAM 即 Visual Simultaneous Localization and
  • linux出现oom分析流程

    背景 linux内核有个机制叫OOM killer Out Of Memory killer xff0c 当系统需要申请内存却申请不到时 xff0c OOM killer会检查当前进程中占用内存最大者 xff0c 将其杀掉 xff0c 腾出
  • 浅入浅出linux中断子系统

    浅入浅出linux中断子系统 xff0c 如需深入 xff0c 直接跳转重要参考章节 什么是中断 xff1f 当CPU被某些信号触发 xff0c CPU暂停当前工作 xff0c 转而处理信号的事件 xff0c 简单的称它为中断 xff0c
  • ROS2交叉编译操作

    ROS2移植过程 在移植ROS2之前 先确认需要移植的版本以及其对应的依赖 这些信息可以在 ROS 2 Releases and Target Platforms 中有介绍 可依据自身需要使用的平台 参考该链接进行选择 下面以ROS2 Hu
  • gstreamer学习笔记---pad定义、连接、流动

    pad相当于element的接口 xff0c 各个element就是通过pad连接进行传输数据 xff0c 同时pad会通过caps限制特定的数据类型通过 xff0c 只有当两个pad的caps数据类型一致时才可以建立连接 那么pad在el
  • gstreamer学习笔记---gst-omx

    一 openMAX理解1 gst omx是基于openMAX开发的插件 xff0c 所以在介绍gst omx之前 xff0c 我们先了解一下openMAX openMAX xff1a open media acceleration xff0
  • csi mipi信号解析

    1 传输模式 LP xff08 Low Power xff09 模式 xff1a 用于传输控制信号 xff0c 最高速率 10 MHz HS xff08 High Speed xff09 模式 xff1a 用于高速传输数据 xff0c 速率
  • 程序调试方法

    记录初衷 xff1a 遇到问题 xff0c 按照一套方法 xff0c 把问题化解 xff0c 逐渐的内化为心法 xff0c 形成经验 xff0c 这就是成长的过程 就好比吃的猪肉 xff0c 经过消化 分解 吸收后变成了自己肉 程序分为三种
  • 博客八:基于xr871实现wifi音响产品

    原创 xff1a http blog csdn net kylin fire zeng xff0c 欢迎转载分享 xff0c 共同进步 xff0c 但请注明出处啊 xff0c 尊重他人成果
  • 用 Docker 部署一个 Python 应用

    Flask项目 这里为了演示的方便 xff0c 我们就写一个简单的Flask项目 xff0c 代码如下 from flask import Flask app 61 Flask name 64 app route 39 39 def ind
  • Realsence D455标定并运行Vins-Fusion

    文章目录 一 双目相机标定1 标定板准备1 1 打印标定板1 2 标定板信息原始pdf的格子参数是 xff1a 调整后的格子参数是 xff1a 2 左右目相机数据准备2 1 修改rs camera launch内容2 2 关闭结构光2 3
  • 【Linux命令】文件和目录权限

    Linux命令 文件和目录权限 权限查看 众所周知 xff0c 可以使用 ls l 来查看文件和目录的详细信息 xff0c 那么输出的东西是什么呢 xff1f 我们先来看 文件类型 xff1a xff1a 普通文件 xff1b d xff1
  • 【设计模式】单例模式

    设计模式 单例模式 1 为什么要用单例 xff1f 单例设计模式 xff08 Singleton Design Pattern xff09 xff1a 一个类只允许创建一个对象 xff08 或实例 xff09 1 1 处理资源访问冲突 例如