单例模式(Singleton Pattern)(三)

简介: 上篇文章我们讲述了,单例的推荐使用方式,以及反射对单例的破坏,但文末留了一个疑问,就是序列化如何破坏一个单例,那么本篇文章就来分析一下。

前言

iShot2022-12-05 00.30.04.png
上文我们了解常见的单例模式的创建方式,但还有没有其他的方式呢?在日常开发中推荐使用哪种呢?本文将带你深入了解其他的单例创建方式,以及单例模式的破坏。

单例模式之静态内部类


静态内部类的方式其实很简单,它就是根据静态内部类的外部类的加载不影响内部类特性,同时解决了按需加载、线程安全的问题,代码也不复杂,总结起来就是:

  • 在静态内部类里创建单例,在装载该内部类时才会去创建单例。
  • 类是由 JVM加载,而JVM只会加载1遍,保证只有1个单例,线程安全。
public class Singleton {

    /**
     * 处理器
     */
    private static class SingletonHandler {
        private static Singleton instance = new Singleton();
    }

    /**
     * 私有构造方法,无法通过new Singleton()方式创建
     */
    private Singleton() {

    }

    /**
     * 通过处理器获取实例
     */
    public static Singleton getInstance() {
        return SingletonHandler.instance;
    }
}

单例模式之枚举

这种方式好在于构造方法会被自动调用,利用这一特性也可以实现单例,默认枚举实例的创建本就是是线程安全的,即使反序列化也不会生成新的实例,任何情况下都是一个单例,满足单例模式所需的:单例创建、线程安全、代码复杂度不高,也是推荐使用的方式。
注意:暴力反射对枚举方式无效哦。

public enum Singleton {

    INSTANCE;

    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

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

反射对于单例的破坏

先看回顾一下Java反射的概念,Java反射是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法。对于任意一个对象,都能够调用它的任意方法和属性。这种动态获取信息、动态调用对象方法的功能就是Java的反射机制。
注意:从上述来看,这种技术过于NiuBi了,其实不仅如此,它还可以可以通过setAccessible方法(通常被称为暴力反射)来修改构造器、字段、方法的可见性。也就是说他可以对类中的私有成员进行操作,那么就破坏了我们的全局访问节点了。

public class Reflect {

    public static void main(String[] args) throws Exception {
            // 首先要获取到该类的Class 对象。
            Class<Singleton> clazz = Singleton.class;

            // getDeclaredConstructor: 不受权限控制的获取类的构造器
            Constructor c = clazz.getDeclaredConstructor(null);

            // 设置为true,就可以对类中的私有成员进行操作了
            c.setAccessible(true);

            Object instance1 = c.newInstance();
            Object instance2 = c.newInstance();

            // false
            System.out.println(instance1 == instance2);
    }
}

public class Singleton {

    private static class SingletonHandler {
        private static Singleton instance = new Singleton();
    }

    private Singleton() {
        
    }

    public static Singleton getInstance() {
        return SingletonHandler.instance;
    }
}
那么如何解决呢?其实也很简单,只需要在单例类的构造方法中,添加判断即可。
public class Singleton {

    private static class SingletonHandler { 
        private static Singleton instance = new Singleton();
    }

    private Singleton() {
        if(SingletonHandler.instance != null) {
            throw new RuntimeException("警告!禁止非法访问!");
        }
    }

    public static Singleton getInstance() {
        return SingletonHandler.instance;
    }
}

为什么枚举类可以阻止反射的破坏?

 
Java规范字规定,每个枚举类型及其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。在序列化的时候Java仅仅是将枚举对象的name属性输到结果中,反序列化的时候则是通过java.lang.Enum的valueOf()方法来根据名字查找枚举对象。
在上文中序列化的时候只将这个INSTANCE名称输出,反序列化的时候再通过这个名称,查找对应的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。
而枚举类中是没有空参构造方法的,反射方法中不予许使用反射创建枚举对象。

总结

由上文所述,静态内部类,利用 Java 的静态内部类来实现单例。这种实现方式,既支持延迟加载,也支持高并发,实现起来也比双重检测简单。而枚举方式是最简单的实现方式,基于枚举类型的单例实现。这种实现方式通过Java的枚举类型它本身的特性,保证了实例创建的线程安全性和实例的唯一性(同时阻止了反射和序列化对单例的破坏),但是破坏单例的不只有反射,想想如果是序列化的方式,有没有问题?

目录
相关文章
|
前端开发 Linux 网络安全
kali linux 网络渗透测试学习笔记(一)Nmap工具进行端口扫描
一.利用Nmap扫描网站服务器的端口开放情况首先打开我们的kali linux才做系统,再打开其命令行模式,输入:nmap www.csdn.net 如下图所示:   因此,通过这个结果可以表明csdn上的端口开放了80端口以及443端口吗,这两个端口都十分重要,因为80端口适用于给用户呈现出数据,给用户看网站用的,443端口也是一个比较重要的端口。
2494 0
|
6月前
|
消息中间件 存储 Kafka
RocketMQ实战—4.消息零丢失的方案
本文分析了用户支付完成后未收到红包的问题,深入探讨了RocketMQ事务消息机制的实现原理及其在确保消息零丢失中的作用。首先,通过全链路分析发现消息可能在推送、存储或消费环节丢失。接着,介绍了RocketMQ事务消息机制如何通过half消息、本地事务执行及回调确认来保证消息发送成功,并详细解析了其底层原理,如half消息对消费者不可见、rollback与commit操作等。同时,对比了同步重试方案,指出其在复杂场景下的局限性。
RocketMQ实战—4.消息零丢失的方案
|
12月前
|
搜索推荐 安全 网络安全
Discuz! X3.5自带参数防御CC攻击以及原理、开启防CC攻击后不影响搜索引擎收录的方法
Discuz! X3.5自带参数防御CC攻击以及原理、开启防CC攻击后不影响搜索引擎收录的方法
512 2
|
机器学习/深度学习 人工智能 测试技术
探索自动化测试的前沿技术与实践
自动化测试作为提升软件开发效率和质量的关键工具,正经历着前所未有的变革。随着人工智能、机器学习、云计算等技术的融合与创新,自动化测试不断突破传统界限,展现出更智能、更高效、更灵活的发展趋势。本文将深入探讨自动化测试领域的最新技术进展,分析其在现代软件开发中的应用,并讨论如何有效整合这些技术以最大化测试效率和准确性。
|
12月前
|
存储 缓存 NoSQL
深入理解后端缓存机制的重要性与实践
本文将探讨在后端开发中缓存机制的应用及其重要性。缓存,作为提高系统性能和用户体验的关键技术,对于后端开发来说至关重要。通过减少数据库访问次数和缩短响应时间,缓存可以显著提升应用程序的性能。本文将从缓存的基本概念入手,介绍常见的缓存策略和实现方式,并通过实例展示如何在后端开发中有效应用缓存技术。最后,我们将讨论缓存带来的一些挑战及其解决方案,帮助您在实际项目中更好地利用缓存机制。
|
存储 缓存 网络协议
|
弹性计算 开发框架 .NET
阿里云服务器和虚拟主机有什么区别?多维度对比
阿里云虚拟主机便于快速建站,适合初学者,预装常用环境如PHP、ASP等,操作简易。云服务器ECS则提供全面权限与弹性扩展能力,不仅限于建站,亦可用于APP、游戏等场景,更适合技术娴熟的用户。尽管虚拟主机价格较低,但鉴于云服务器性能优越且价格持续下降,推荐选择云服务器以获取更佳性价比与广泛的应用场景。
411 3
Springboot byte[] 转 MultipartFile ,InputStream 转 MultipartFile
Springboot byte[] 转 MultipartFile ,InputStream 转 MultipartFile
1691 0
|
安全 区块链 数据安全/隐私保护
链游开发正式版丨链游系统开发指南教程丨链游系统源码程序
链游系统开发规则指的是在构建基于区块链技术的游戏系统时需要遵循的一系列规定和准则。这些规则旨在确保链游系统的安全性、可靠性、透明性和用户体验,
|
JavaScript Java 应用服务中间件
一张思维导图带你学会SpringBoot、Vue前后端分离项目线上部署(一)
一张思维导图带你学会SpringBoot、Vue前后端分离项目线上部署
685 0