24种设计模式之单例模式(饿汉式、懒汉式)

2023-11-18

一、单例模式

单例模式( Singleton Pattern )是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛,例如,总统,班主任等。J2EE标准中的ServletContext 、ServletContextConfig 等、Spring框架应用中的。

  • 特点:构造方法私有,提供一个全局访问点。

  • 实现方式:有很多,1.饿汉式 2.懒汉式 3.注册式 4.ThreadLocal

  • 优点:内存中只有一个实例,减少内存开销;避免对资源多重占用;设置全局访问点,严格控制访问。

  • 缺点:没有接口,扩展困难;如果要扩展单例对象,只有修改代码,没有其他途径,不符合程序的开闭原则。

二、饿汉式单例模式

饿汉式单例模式在类加载的时候就立即初始化,并且创建单例对象。它绝对线程安全,在线程还没出现以前就实例化了,不可能存在访问安全问题。

总结:

final:防止反射破坏单例。

饿汉式缺点:可能会造成内存空间的浪费。

饿汉式单例模式适用于单例对象较少的情况。这样写可以保证绝对线程安全、执行效率比较高。但是它的缺点也很明显,就是所有对象类加载的时候就实例化。这样一来,如果系统中有大批量的单例对象存在,那系统初始化是就会导致大量的内存浪费。

饿汉式之单例实现方式

1、标准饿汉模式

通过私有构造器,防止外部进行实例创建;通过属性在类加载时实例化对象,提供全局访问方法取得实例。利用代码的执行先后顺序,在线程还没有出现前就完成了实例化。

public class HungrySingleton {
    // 静态实例代码段,饿汉实现类加载初始化时调用构造方法
    private static final HungrySingleton hungrySingleton = new HungrySingleton();
     
    // 私有方法防止外部调用创建对象
    private HungrySingleton() {}

    // 外部类获得单例对象方法
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

该单例实现方式可以被反序列化和反射破坏:

(1)反射破坏方式如下:该方式可以通过构造方法创建出一个全新的实例对象。

public static void reflect() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        System.out.println(Test.getInstance());
        //反射破坏
        //得到类
        Class c = Test.class;
        Constructor<?> constructor = c.getDeclaredConstructor();
        //设置私有可调用
        constructor.setAccessible(true);
        // 打印创建的实例对象
        System.out.println(constructor.newInstance());

    }

可见该方法是通过调用构造方法创建出一个新的对象。

(2)反序列化破坏单例方式如下:

public static void ser() throws IOException, ClassNotFoundException {
        //反序列化
        ByteArrayOutputStream outputStream=new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream=new ObjectOutputStream(outputStream);
        //将类转化
        objectOutputStream.writeObject(Test.getInstance());
        System.out.println(Test.getInstance());
        ObjectInputStream objectInputStream=new ObjectInputStream(new ByteArrayInputStream(outputStream.toByteArray()));
        //读出类,变为一个新的类
        Test test= (Test) objectInputStream.readObject();
        System.out.println(test);
    }

该方式可以看出反序列化构造出的对象并不是通过构造方法。

由此针对上面两种破坏方式做出优化得到以下的代码:

public class Test implements Serializable {

    //静态实例代码段,饿汉实现类加载初始化时调用构造方法
    private static Test Instance=new Test();

    //私有方法防止外部调用创建对象
    private Test(){
        if(Instance!=null)// 此处方式反射调用破环单例对象,抛出异常
            throw new RuntimeException("单例模式不能创建");
        System.out.println("构造方法");
    }

    //外部类获得单例对象方法
    public static Test getInstance(){
        return Instance;
    }
    //其他方法
    public static void otherMethod(){
        System.out.println("other");
    }

    //防止反序列化破坏单例
    public Object readResolve(){
        return  Instance;
    }
}
2、静态代码块机制
public class HungryStaticSingleton {
    // 静态志方式饿汉式单例
    private static final HungryStaticSingleton hungrySingleton ;
    static {
        hungrySingleton = new HungryStaticSingleton();
    }
     
    /**
     * 私有构造
     */
    private HungryStaticSingleton() {}

    //取实例方法
    public static HungryStaticSingleton getInstance() {
        return hungrySingleton;
    }
}
3、枚举类实现饿汉:枚举类实现方式不会被反射和反序列化破环单例
public enum  Test_1 {
    Instance;

    //枚举类默认构造方法私有
    Test_1(){
        System.out.println("构造方法");
    }

    //获取对象
    public static Test_1 getInstance(){
        return Instance;
    }

    //其他方法
    public static void otherMethod(){
        System.out.println("other");
    }
}

三、懒汉式单例模式

懒汉式类被加载的时候,没有立刻被实例化,第一次调用getInstance的时候,才真正的实例化。

如果要是代码一整场都没有调用getInstance,此时实例化的过程也就被省略掉了,又称“延时加载”

一般认为“懒汉模式” 比 “饿汉模式”效率更高。

懒汉模式有很大的可能是“实例用不到”,此时就节省了实例化的开销。

懒汉式之单例实现方式

1、普通的懒汉式
public class LazySingleton {
    private LazySingleton() {
    }

    private volatile static LazySingleton instance;

    //加入了同步代码,解决线程不安全问题
    public synchronized static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

这种设计明显的一个问题就是执行效率低,无论是否已经存在实例,在多线程的情况下都会发生阻塞。

对以上代码进行改进,首先让当程序中实例存在的时候,直接返回实例,不需要抢占锁。当程序中不存在实例时,再抢占锁进行创建。根据以上的思想,出现了第二种懒汉式方式:

2、双重检查锁DCL(Double Check Lock双端检锁)
public class LazyDoubleCheckSingleton {
    private LazyDoubleCheckSingleton() {
    }

    private volatile static LazyDoubleCheckSingleton instance;

    public static LazyDoubleCheckSingleton getInstance() {
        //确定是否需要阻塞
        if (instance == null) {
            // 线程安全:双重检查锁(同步代码块)
            synchronized (LazyDoubleCheckSingleton.class) {
                //确定是否需要创建实例
                if (instance == null) {
                    //这里在多线程的情况下会出现指令重排的问题,所以对共有资源instance使用关键字volatile修饰
                    instance = new LazyDoubleCheckSingleton();
                }
            }
        }
        return instance;
    }

}

对于第二种方式,较第一种方式而言,性能提高了,但是代码的可读性差了。


DCL(Double Check Lock双端检锁)机制不一定线程安全,原因是有指令重排序的存在,加入volatile可以禁止指令重排。

原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化

instance = new LazyDoubleCheckSingleton();可以分为以下3步完成(伪代码)

memory = allocate(); // 1.分配对象内存空间

instance(memory); // 2.初始化对象

instance=memory; // 3.设置instance指向刚分配的内存地址,此时instance != null

步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。

memory = allocate(); // 1.分配对象内存空间

instance=memory; // 3.设置instance指向刚分配的内存地址,此时instance != null,但是对象还没有初始化完成

instance(memory); // 2.初始化对象

但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。

所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。

3、静态内部类
 /**
 * 使用静态内部类,性能最优
 */
public class LazyInnerClassSingleton {
    //虽然构造方法私有了,但是逃不过反射的法眼
    private LazyInnerClassSingleton(){};
 
    // 懒汉式单例
    // LazyHoler里面的逻辑需等外部方法调用时候才执行
    // 巧妙运用了内部类的特性
    // JVM底层逻辑,完美避免了线程安全问题
    public static final LazyInnerClassSingleton getInstance(){
        return LazyHoler.LAZY;
    }
 
    public static class LazyHoler{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

为防止调用者反射破坏,可以这么写:

public class LazyInnerClassSingleton {
    //虽然构造方法私有了,但是逃不过反射的法眼
    private LazyInnerClassSingleton(){
        // 防止调用者反射攻击; 
        if(LazyHoler.LAZY != null){
            throw new RuntimeException("禁止创建多个实例!"); // 其他写法也可加上
        }
    };
 
    // 懒汉式单例
    // LazyHoler里面的逻辑需等外部方法调用时候才执行
    // 巧妙运用了内部类的特性
    // JVM底层逻辑,完美避免了线程安全问题
    public static final LazyInnerClassSingleton getInstance(){
        return LazyHoler.LAZY;
    }
 
    public static class LazyHoler{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

分析:静态内部类相对来说更优,LazyHoler里面的逻辑需等外部方法调用时候才执行,所以也属于懒汉式,巧妙运用了内部类的特性,JVM底层逻辑,完美避免了线程安全问题,

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

24种设计模式之单例模式(饿汉式、懒汉式) 的相关文章

随机推荐

  • STM32驱动HX711称重模块

    使用模块如下图所示 使用单片机为STM32C8T6 引脚DT gt PB7 SCK gt PB6 使用通道A 下面是驱动程序 void GPIO Weigh Init void GPIO InitTypeDef GPIO InitStruc
  • 设计模式-单一职责原则介绍与理解

    描述 一个类应该专注于实现一个功能 好处 便于代码复用 举例 俄罗斯方块游戏 首先可以想到的是游戏逻辑与界面的分离 也就是说逻辑一个类 界面部分一个类 这样做的好处就是我们可以复用游戏逻辑的代码 例如我们用java写了一个基于PC端的俄罗斯
  • JAVA实现压缩解压文件

    1 源码运行前准备好文件夹 2 源码 package com els modules inquiry service impl import java io File import java io FileInputStream impor
  • P2PSim中重要函数的说明

    环境 RedHat9上安装的P2Psim0 3 目的 在P2Psim使用Vivaldi协议仿真 现状 主程序代码中关于vivaldi协议的部分注释掉了 思路 从主函数分析代码 找到原因 vivaldi协议主函数是vivalditest C
  • windows server 2012R2 部署安装 hmail

    windows server 2012R2 部署安装 hmail 环境说明 系统 windows server2012 R2软件版本 hMailServer 5 6 7 B2425 exe 邮件客户端 foxmail7 2版本 加密工具 h
  • Python21天打卡Day20-可变参数、关键字参数

    在 Python 中 可变参数允许函数接受任意数量的参数 这些参数被封装成一个元组 Tuple 或列表 List 并作为参数传递给函数 Python 中有两种类型的可变参数 args 用于传递可变数量的位置参数 Positional Arg
  • 【Docker系列】从头学起 Docker——docker run 命令详解

    文章目录 作用 语法格式 docker run 执行流程 options 说明 实际例子 例一 例二 例三 例四 例五 例六 例七 例八 总结 例九 作用 创建一个新的容器并运行一个命令 语法格式 docker run OPTIONS IM
  • springboot项目打包(exe+jdk/jre+mysql)跨平台一键安装

    SpringBoot项目打包 exe jdk jre mysql 跨平台一键安装 1 Spring Boot将javaFX应用打包为jar包 1 1 pom xml安装Spring boot maven plugin
  • 【MyBatis-Plus】详解Wrappers.<T> lambdaQuery()以及常用过滤方法

    Wrappers
  • Java 动态代理作用是什么?

    主要用来做方法的增强 让你可以在不修改源码的情况下 增强一些方法 在方法执行前后做任何你想做的事情 甚至根本不去执行这个方法 因为在 InvocationHandler的invoke方法中 你可以直接获取正在调用方法对应的 Method对象
  • linux kernel --component组件用法

    kernel component组件用法 linux component组件架构分析
  • 如何用mac搭建本地svn服务器(如何将mac变成版本管理服务器)

    前言 一 搭建本地svn服务器 1 建立代码库 2 配置文件修改 3 启动本地svn服务 二 搭建过程中常见问题 如果Mac os升级到10 0以上 自带的svn不支持了怎么办 三 mac本地使用svn软件管理svn库 cornerston
  • Linux多进程数据交换--共享内存

    个人博客地址 https cxx001 gitee io 基础 在linux系统开发当中 时常需要在多个进程之间交换数据 在多个进程之间交换数据 有很多方法 但最高效的方法莫过于共享内存 linux共享内存是通过tmpfs这个文件系统来实现
  • 第二十八节、基于深度学习的目标检测算法的综述(附代码,并附有一些算法英文翻译文章链接))...

    在前面几节中 我们已经介绍了什么是目标检测 以及如何进行目标检测 还提及了滑动窗口 bounding box 以及IOU 非极大值抑制等概念 这里将会综述一下当前目标检测的研究成果 并对几个经典的目标检测算法进行概述 本文内容来自基于深度学
  • APK 逆向工程 - 解析 apk 基本信息和方法调用图

    导读 在 Android 开发中 我们很少使用 Android 逆向去分析 apk 文件的 但是作为一个测试人员 我们要对这个 apk 文件进行一系列的分析 审核 测试 这篇文章讲解如何解析一个 apk 文件 主要从下面几方面介绍 解析前准
  • mysql show para_mysql中show命令的详细用法

    经过我测试的语句 show procedure status 显示数据库中所有存储的存储过程基本信息 包括所属数据库 存储过 程名称 创建时间等 show create procedure sp name 显示某一个存储过程的详细信息 a
  • MongoDB安装和批量写入

    本文主要以Ubuntu系统为例 记录安装部署MongoDB社区版 并进行批量数据写入 安装部署主要依据MongoDB官网指引 数据写入脚本为个人编写 如有需要可以直接使用 1 导入包管理系统使用的公钥 wget qO https www m
  • UML中依赖和关联,关联,聚合和组合的区别

    在UML中 依赖和关联经常无法进行区分 在类图中 不知道什么时候使用依赖 什么时候使用关联 来定义两个类之间的关系 今天看了一篇帖子 对这两种关系做了比较生动的区分 依赖指的是两个类之间发生的关系输入偶然发生的 例如人和船之间的关系就是这种
  • Video_Codec_SDK压缩编码视频并封装为MP4格式

    在深度学习处理视频过程中 通常是先解码视频获取视频帧并转为cv Mat后进行处理 本章介绍如何将处理后的图片使用GPU编码为视频码流并封装为MP4格式 开发硬件 I7 9750H GTX1660ti 操作系统 Ubuntu16 04 驱动版
  • 24种设计模式之单例模式(饿汉式、懒汉式)

    一 单例模式 单例模式 Singleton Pattern 是指确保一个类在任何情况下都绝对只有一个实例 并提供一个全局访问点 单例模式是创建型模式 单例模式在现实生活中应用也非常广泛 例如 总统 班主任等 J2EE标准中的ServletC