【Java设计模式】二、单例模式

文章目录

  • 0、单例模式
  • 1、饿汉式
  • 2、懒汉式
  • 3、双重检查
  • 4、静态内部类
  • 5、枚举
  • 6、单例模式的破坏:序列化和反序列化
  • 7、单例模式的破坏:反射
  • 8、单例模式的实际应用

设计模式即总结出来的一些最佳实现。GoF(四人组) 书中提到23种设计模式,可分为三大类:

  • 创建型模式:隐藏了创建对象的过程,通过逻辑方法进行创建对象,使用者不用关注对象的创建细节(对那种属性很多,创建麻烦的对象尤其好用)
  • 结构型模式:主要关注类和对象的组合关系。将类或对象按某种布局组成更大的结构
  • 行为型模式:主要关注对象之间的通信与配合

在这里插入图片描述

0、单例模式

  • 单例模式即在程序中想要保持一个实例对象,让某个类只有一个实例
  • 单例类必须自己创建自己唯一的实例,并对外提供
  • 优点:减少了内存开销

单例模式的实现,有以下几种思路:

1、饿汉式

  • 类加载就会导致该单实例对象被创建
  • 通过静态代码块或者静态变量直接初始化

方式一:静态成员变量的方式

public class HungrySingleton {

    private static final HungrySingleton hungrySingleton = new HungrySingleton();
	
	//私有的构造方法,只能本类调用,不给外界用
    private HungrySingleton(){

    }
	//提供一个获取实例的方法给外界
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}


方式二:静态代码块

public class HungrySingleton {

   
    private static HungrySingleton hungrySingleton = null;

    //  静态代码块中进行赋值
    static {
        hungrySingleton = new HungrySingleton();
    }
	//私有的构造方法,只能本类调用,不给外界用
    private HungrySingleton(){

    }
	//提供一个获取实例的方法给外界
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}


以上两种方式,对象会随着类的加载而创建,如果这个对象后来一直没被用,就有点白占内存了。

2、懒汉式

类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建


懒汉式方式一:线程不安全
public class LazySingleton  {
    private static LazySingleton lazySingleton = null;

    /**
     * 私有的构造方法,保证出了本类就不能再被调用,以防直接去创建对象
     */
    private LazySingleton() {

    }

    /**
     * 单例对象的获取
     */
    public static LazySingleton getInstance() {
        if (lazySingleton == null) {
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }

}

测试类启两个线程获取对象:

public class Test {
    public static void main(String[] args) {
        new Thread(() -> {
            LazySingleton instance = LazySingleton.getInstance();
            System.out.println(Thread.currentThread().getName() + "-->" + instance);
        }, "t1").start();
        new Thread(() -> {
            LazySingleton instance = LazySingleton.getInstance();
            System.out.println(Thread.currentThread().getName()+ "-->" + instance);
        }, "t2").start();
    }
}

发现可能出现获取到两个不同对象的情况,这是因为线程安全问题:

在这里插入图片描述

两个线程A、B,同时执行IF 这一行,被挂起,再被唤醒时继续往下执行,就会创建出两个不同的实例对象。那最先想到的应该是synchronized关键字解决,但这样性能低下,因为不管对象是否为null,每次都要等着获取锁。

//性能低下,一刀切,不可行
public static synchronized LazySingleton getInstance() {
     if (lazySingleton == null) {
         lazySingleton = new LazySingleton();
     }
     return lazySingleton;
 }

3、双重检查

通过两个IF判断,加上同步锁进行实现。(懒汉式方式二:双重检查)

public class DoubleCheckSingleton {

    private static DoubleCheckSingleton doubleCheckSingleton;

    /**
     * 私有的构造方法,保证出了本类就不能再被调用,以防直接去创建对象
     */
    private DoubleCheckSingleton(){

    }
    public static DoubleCheckSingleton getInstance(){
        if(doubleCheckSingleton == null){
            synchronized (DoubleCheckSingleton.class){
                if(doubleCheckSingleton == null){
                    doubleCheckSingleton = new DoubleCheckSingleton();
                }
            }
        }
        return doubleCheckSingleton;
    }
}

如此,再有A、B两个线程同时执行到第一个IF,只能有一个成功创建对象,另一个获取到锁后,第二重判断会告诉它已经有对象实例了。而亮点则在于,对象初始化完成后,后面来获取对象的线程不用等着拿锁,第一个IF就能告诉它已有对象,不用再等锁了。


以上双端检锁虽然线程安全,但问题是,JVM指令重排序后,可能出现空指针异常,可再加volatile关键字(volatile的可见性和有序性,这里用它的有序性)

//...
private static volatile DoubleCheckSingleton doubleCheckSingleton;
//...

4、静态内部类

在单例类中,通过私有的静态内部类,创建单例对象(加private修饰词的,出了本类无法调用和访问)。这也是懒汉式的第三种方式。

public class StaticInnerClassSingleton {

    /**
     * 私有的静态内部类实例化对象
     * 给内部类的属性赋值一个对象
     */
    private static class InnerClass{
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }

    /**
     * 私有的构造方法,保证出了本类就不能再被调用,以防直接去创建对象
     */
    private StaticInnerClassSingleton(){

    }
	/**
     * 对外提供获取实例的方法
     */
    public static StaticInnerClassSingleton getInstance(){
        return InnerClass.staticInnerClassSingleton;
    }
}

外部类StaticInnerClassSingleton被加载时,其对象不一定被初始化,因为内部类没有被主动使用到。直到调用getInstance方法时,静态内部类InnerClass被加载,完成实例化。

静态内部类在被加载时,不会立即实例化,而是在第一次使用时才会被加载并初始化。

JVM在加载外部类的过程中,不会加载静态内部类,只有内部类的属性或方法被调用时才会被加载,并初始化其静态属性。 这种延迟加载的特性,使得我们可以通过静态内部类来实现在需要时创建单例对象。这种方式没有线程安全问题,也没有性能影响和空间的浪费。

5、枚举

  • 单例模式的最佳实现方式
  • 枚举类型是线程安全的,并且只会装载一次
  • 可有效防止对单例模式的破坏
  • 属于饿汉式
public enum EnumSingleton {
    INSTANCE;

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

6、单例模式的破坏:序列化和反序列化

通过流将单例对象,序列化到文件中,然后再反序列化读取出来。发现通过反序列化方式创建出来的对象内存地址,和原对象不一样。或者对象序列化到文件后,两次反序列化得到的对象也不一样,单例模式被破坏。

public class TestSerializer {

    public static void main(String[] args) throws Exception {
        //懒汉式
        LazySingleton instance = LazySingleton.getInstance();
        //把对象序列化到文件中
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton"));
        oos.writeObject(instance);
        //从文件中反序列化对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton"));
        LazySingleton objInstance = (LazySingleton) ois.readObject();
        System.out.println(instance);
        System.out.println(objInstance);
        System.out.println(instance == objInstance);
    }
}

在这里插入图片描述

可以发现单例模式的五种实现方式中,只有枚举不会被破坏单例模式。如果非要用其他几种模式,可以加readResolve方法来重写反序列化逻辑。因为反序列化创建对象时,是通过反射创建的,反射会调用readResolve方法,并将其返回值做为反序列化的结果。 没有重写readResolve方法时,会通过反射创建一个新的对象,从而破坏了单例模式。这一点在对象流ObjectInputStream的源码可看出:

在这里插入图片描述

public class LazySingleton implements Serializable {
    private static LazySingleton lazySingleton = null;

    /**
     * 私有的构造方法,保证出了本类就不能再被调用,以防直接去创建对象
     */
    private LazySingleton() {
    }

    /**
     * 单例对象的获取
     */
    public static LazySingleton getInstance() {
        if (lazySingleton == null) {
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }

    private Object readResolve(){
        return lazySingleton;
    }


}

在这里插入图片描述

也可考虑使用@JsonCreator注解。

7、单例模式的破坏:反射

  • 通过字节码对象,创建构造器对象
  • 通过构造器对象,初始化单例对象
  • 由于单例对象的构造方法是private私有的,调用构造器中的方法,赋予权限,创建单例对象

注意私有修饰词时,反射会IllegalAccessException

在这里插入图片描述

处理下private问题,用懒汉模式验证:

public class TestReflect {
    public static void main(String[] args) throws Exception{
        //创建字节码对象
        Class<LazySingleton> clazz = LazySingleton.class;
        //构造器对象
        Constructor<LazySingleton> constructor = clazz.getDeclaredConstructor();
        //赋予权限
        constructor.setAccessible(true);
        //解决了私有化问题,获取实例对象
        LazySingleton o1 = constructor.newInstance();
        //再次反射
        LazySingleton o2 = constructor.newInstance();
        System.out.println(o1);
        System.out.println(o2);
        System.out.println(o1 == o2);

    }
}

在这里插入图片描述

用枚举的方式验证:

public class TestEnumReflect {
    public static void main(String[] args) throws Exception {
        Class<EnumSingleton> clazz = EnumSingleton.class;
        //枚举下的单例模式,创建构造方法时,需要给两个参数,薮泽NoSuchMethodException
        //这两个参数是源码中的体现,一个是String,一个是int
        Constructor<EnumSingleton> constructor = clazz.getDeclaredConstructor(String.class, int.class);
        constructor.setAccessible(true);
        EnumSingleton instanceReflect = constructor.newInstance("test",1234);
        EnumSingleton instanceSingleton = EnumSingleton.getInstance();
        System.out.println(instanceReflect);
        System.out.println(instanceSingleton);
        System.out.println(instanceReflect == instanceSingleton);

    }
}

运行报错:Cannot reflectively create enum objects,即反射创建枚举的单例对象,是不允许的:

在这里插入图片描述

在其他单例模式的实现方式里,也可以实现不允许通过反射创建对象,反射靠拿构造方法对象,调整下构造方法(比如双重检锁):

public class DoubleCheckSingleton {

    private static DoubleCheckSingleton doubleCheckSingleton;

    /**
     * 私有的构造方法,保证出了本类就不能再被调用,以防直接去创建对象
     */
    private DoubleCheckSingleton(){
		if (doubleCheckSingleton != null) {
			throw new RuntimeException("不允许创建多个对象");
		}
    }
    public static DoubleCheckSingleton getInstance(){
        if(doubleCheckSingleton == null){
            synchronized (DoubleCheckSingleton.class){
                if(doubleCheckSingleton == null){
                    doubleCheckSingleton = new DoubleCheckSingleton();
                }
            }
        }
        return doubleCheckSingleton;
    }
}

或者:

在这里插入图片描述

8、单例模式的实际应用

JDK的Runtime类就是饿汉式的单例模式:

在这里插入图片描述

PS:Runtime类的简单使用 --> 执行DOS命令并获取结果

public class RuntimeDemo {
	public static void main(String[] args) throws IOException {
		//获取Runtime类对象
		Runtime runtime = Runtime.getRuntime();
		//返回 Java 虚拟机中的内存总量。
		System.out.println(runtime.totalMemory());
		//返回 Java 虚拟机试图使用的最大内存量。
		System.out.println(runtime.maxMemory());
		//创建一个新的进程执行指定的字符串命令,返回进程对象
		Process process = runtime.exec("ipconfig");
		//获取命令执行后的结果,通过输入流获取
		InputStream inputStream = process.getInputStream();
		byte[] arr = new byte[1024 * 1024* 100];
		//将流输入到数组,返回读到的字节个数
		int b = inputStream.read(arr); 
		//字节数组转字符串,指定下字符集为GBK
		System.out.println(new String(arr,0 ,b ,"gbk"));
	}
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

linux c++ 开发 tensorrt 安装

tensorrt 官方下载地址&#xff08;需要注册账号登录&#xff09;&#xff1a;Log in | NVIDIA Developer 根据系统发行版和CUDA版本 (nvcc -V) 选择合适的安装包 EA&#xff08;early access&#xff09;版本代表抢先体验。 GA&#xff08;general availability&#xff09;代…

文件对比工具Beyond Compare 4 mac v4.4.7(28397)中文版

Beyond Compare是一款适用于Windows、Mac OS X和Linux平台的文件和文件夹比较工具。它可以帮助用户比较和同步文件夹、文件和压缩包等内容&#xff0c;支持多种文件格式&#xff0c;如文本、图像、音频、视频等。 软件下载&#xff1a;Beyond Compare 4 mac v4.4.7(28397)中文版…

如何在Node.js中使用定时器

在Node.js中使用定时器是一项常见且重要的任务&#xff0c;特别是在需要执行定时任务或者轮询操作的情况下。Node.js提供了多种方式来实现定时器功能&#xff0c;包括setTimeout、setInterval和setImmediate等方法。本篇博客将介绍如何在Node.js中使用这些定时器&#xff0c;并…

WPF真入门教程30--顺风物流单据管理系统

1、教程回顾 到现在为止&#xff0c;真入门系列教程已完成了29刺由浅入深地讲解&#xff0c;当然不可能讲到了WPF的所有技能点&#xff0c;但读者看到了wpf的内部各种功能及之间的联系&#xff0c;在此基础上&#xff0c;提供一个完整有效的综合项目&#xff0c;本项目采用的是…

期货开户保证金保障市场正常运转

期货保证金是什么&#xff1f;在期货市场上&#xff0c;采取保证金交易制度&#xff0c;投资者只需按期货合约的价值&#xff0c;交一定比率少量资金即可参与期货合约买卖交易&#xff0c;这种资金就是期货保证金。期货保证金&#xff08;以下简称保证金〕按性质与作用的不同。…

AGV搬运机器人能给企业带来哪些效益?

agv 当前物流行业正在以每年40%的速度快速增长&#xff0c;却依然是典型的劳动密集型行业。随着机器人技术的崛起&#xff0c;传统物流行业也开始加大对物流科技设备的研发。AGV机器人被广泛应用于整个仓储系统内&#xff0c;疏解了一部分人力的负担&#xff0c;使后台工作更加…

使用Azure下载数据集方法

首先需要获取到下载的链接&#xff0c;例如&#xff1a; https://aimistanforddatasets01.blob.core.windows.net/cocacoronarycalciumandchestcts-2?sv2019-02-02&src&sigHvhvAtJ7KRr1uIZkjkANqozGvOsqlamMDOKcQegYLrw%3D&st2024-02-29T11%3A55%3A45Z&se2024…

水豚鼠标助手 强大的鼠标美化工具

水豚鼠标助手 水豚鼠标助手是一款 鼠标换肤、屏幕画笔、放大镜、聚光灯、屏幕放大、倒计时功能的强大屏幕演示工具。 软件助手获取 水豚鼠标助手1.0.0 安装教程 第一步&#xff1a;下载后&#xff0c;双击软件安装包 第二步&#xff1a;Windows可能会出现提示弹窗&#xff…

Mac 制作可引导安装器

Mac 使用U盘或移动固态硬盘制作可引导安装器&#xff08;以 Monterey 为例&#xff09; 本教程参考 Apple 官网相关教程 创建可引导 Mac OS 安装器 重新安装 Mac OS 相关名词解释 磁盘分区会将其划分为多个单独的部分&#xff0c;称为分区。分区也称为容器&#xff0c;不同容器…

Docker(运维工具)—— 学习笔记

快速构建、运行、管理应用的工具 一、安装docker 参考Install Docker Engine on Ubuntu | Docker Docs 二、快速入门 1、镜像和容器 docker镜像可以做到忽略操作系统的差异&#xff0c;跨平台运行&#xff0c;忽略安装的差异 当我们利用Docker安装应用时&#xff0c;Dock…

环境配置 |Jupyter lab/Jupyter Notebook 安装与设置

ipynb使用Jupyterlab/Jupyter Notebook 来编写Python程序时的文件,在使用时,可以现转换为标准的.py的python文件 1.Jupyter Lab 1.1.下载安装 环境&#xff1a;Linux pip install jupyterlab 1.2.使用 jupyter lab 点击后进入 1.3.jupyter lab更换内核 因为我的是在anac…

3d图形学基础(一):向量与坐标系

文章目录 1.1 向量与坐标系1.1.1 向量与坐标系的应用1.1.2 完整测试代码 1.1 向量与坐标系 1.1.1 向量与坐标系的应用 零向量&#xff1a; 零向量是没有方向的向量&#xff1b; 负向量&#xff1a; 负向量是与原向量方向相反、长度相等的向量&#xff1b; 向量的模&#xf…

MySQL学习Day24—数据库的设计规范

一、数据库设计的重要性: 1.糟糕的数据库设计产生的问题: (1)数据冗余、信息重复、存储空间浪费 (2)数据更新、插入、删除的异常 (3)无法正确表示信息 (4)丢失有效信息 (5)程序性能差 2.良好的数据库设计有以下优点: (1)节省数据的存储空间 (2)能够保证数据的完整性 …

matlab:涉及复杂函数图像的交点求解

matlab&#xff1a;涉及复杂函数图像的交点求解 在MATLAB中求解两个图像的交点是一个常见的需求。本文将通过一个示例&#xff0c;展示如何求解两个图像的交点&#xff0c;并提供相应的MATLAB代码。 画出图像 首先&#xff0c;我们需要绘制两个图像&#xff0c;以便直观地看…

模拟算法题练习(二)(DNA序列修正、无尽的石头)

&#xff08;一、DNA序列修正&#xff09; 问题描述 在生物学中&#xff0c;DNA序列的相似性常被用来研究物种间的亲缘关系。现在我们有两条 DNA序列&#xff0c;每条序列由 A、C、G、T 四种字符组成&#xff0c;长度相同。但是现在我们记录的 DNA序列存在错误&#xff0c;为了…

Elasticsearch入门-环境安装ES和Kibana以及ES-Head可视化插件和浏览器插件es-client

Elasticsearch入门-环境安装ES和Kibana 安装 ES Windows安装ESHead安装浏览器插件 es-clientKibana 安装 安装es,安装header 安装kibana&#xff0c;安装多种分词器ik… 安装 ES Windows安装 ① 下载压缩包并解压官网链接&#xff1a;https://www.elastic.co/cn/downloads/ela…

【MATLAB】兔子机器人腿部_simulink模型解读(及simulink中的simscape的各模块介绍)

一、动力学模型 总系统引脚含义 关节电机 Fcn 搭建方程&#xff0c;输入与输入方程 phi1 -q 大腿 小腿同理 车轮 另一边对称 虚拟腿传感器 二、控制模型 VMC解算五连杆 Pulse Generator 腿长控制器PID leg_conv.m&#xff1a;可由虚拟腿目标扭矩和推力求得电机所需…

通过jenkins进行部署java程序到centos上

1.通过jumpserver访问到centos上&#xff0c;准备下java环境 // step1: 先编辑下 vim /etc/profile// step2: 编写好环境变量 JAVA_HOME/usr/local/java export JAVA_HOME export ZOOKEEPER_HOME/opt/zookeeper/apache-zookeeper-3.7.0-bin PATH$PATH:$JAVA_HOME/bin:$ZOOKEEP…

正信晟锦:借了钱的人一直不接电话不回信息咋办

在金钱往来中&#xff0c;遇到借出的钱款无法按时回收&#xff0c;且借款人如同人间蒸发一般不接电话、不回信息&#xff0c;确实让人焦虑。面对这种情形&#xff0c;我们需采取明智而有效的措施&#xff0c;以保护自身的权益。 首要策略是保持冷静&#xff0c;不要让情绪主导行…

民间最大的天涯社区宣布:今起,全面恢复!

想到去年一则不起眼的消息&#xff1a;天涯社区已经无法打开。 时代抛弃你的时候&#xff0c;都不说一声再见&#xff0c;现实就是这样残酷。 在互联网的浩瀚星河中&#xff0c;天涯社区曾是最亮的那颗星星&#xff0c;见证了无数网友的青春岁月。 记得我读大学的时候&#xff…