单例模式以及反射对单例模式的破坏及防御

2024-09-08 06:20

本文主要是介绍单例模式以及反射对单例模式的破坏及防御,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

单例模式(Singleton Pattern)是一种确保类在应用程序生命周期内只存在一个实例的设计模式。它不仅提供了全局访问点,还能节省内存、控制实例的生命周期。但常见的单例模式实现方式如饿汉式、懒汉式、双重校验锁、静态内部类等,虽然设计良好,但都容易被 Java 的反射机制所破坏。本文将介绍这些单例实现方式的优缺点、反射如何破坏它们的唯一性,以及如何防御这种破坏。


1. 单例模式的常见实现方式

1.1 饿汉式单例

饿汉式单例模式的特点是类加载时就创建单例对象,即使应用程序从未使用过它。这种方式简单、线程安全,但由于实例是在类加载时创建的,无论是否使用都会占用内存,导致一定的资源浪费。

public class Singleton {private static final Singleton INSTANCE = new Singleton(); // 类加载时创建实例private Singleton() {}  // 私有构造函数public static Singleton getInstance() {return INSTANCE;}
}

优点:

  • 实现简单,类加载时就完成实例化,线程安全。

缺点:

  • 资源浪费:即使不使用,实例也会加载,占用内存。
1.2 懒汉式单例

懒汉式单例模式通过延迟加载,在第一次调用 getInstance() 方法时才创建实例。为了保证线程安全,它使用了 synchronized 关键字锁住方法,虽然解决了并发问题,但每次调用都会锁住,导致性能问题。

public class Singleton {private static Singleton instance;private Singleton() {}  // 私有构造函数public static synchronized Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}
}

优点:

  • 懒加载:只有在需要时才创建实例,避免资源浪费。

缺点:

  • 性能低下:使用 synchronized 锁定整个方法,每次只能有一个线程访问,导致并发性能差。
1.3 双重校验锁单例

为了提升懒汉式的并发性能,双重校验锁模式(Double-Check Locking)仅在实例为 null 时才进行同步操作。通过 volatile 关键字保证对象在多个线程下的可见性,防止指令重排序问题,确保实例在多线程环境下的正确性。

public class Singleton {private static volatile Singleton instance;private Singleton() {}  // 私有构造函数public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}

优点:

  • 提高并发性能:使用局部同步块,减少不必要的同步操作。
  • 解决指令重排序:通过 volatile 防止对象未初始化问题。

缺点:

  • 实现复杂:相比懒汉式和饿汉式,双重校验锁的实现较为复杂,增加了维护成本。
1.4 静态内部类单例

静态内部类实现方式则结合了饿汉式的线程安全性和懒汉式的延迟加载特性。它通过 Java 类加载机制来确保线程安全,并且只有在调用 getInstance() 方法时才会加载静态内部类并实例化单例对象,相比双重校验锁更加优雅。

public class Singleton {private Singleton() {}  // 私有构造函数private static class SingletonHolder {private static final Singleton INSTANCE = new Singleton();}public static Singleton getInstance() {return SingletonHolder.INSTANCE;}
}

优点:

  • 延迟加载:静态内部类只有在被调用时才会加载。
  • 线程安全:由类加载机制保证线程安全,无需显式同步。

缺点:

  • 实现相对较为隐蔽,初学者可能不易理解。

若是有同学觉得静态内部类和饿汉式没啥差别的同学可以看看这篇博客的讲解–>饿汉式VS静态内部类


2. 反射如何破坏单例模式

反射是 Java 提供的一种功能强大的机制,允许在运行时动态访问类的私有成员、构造函数和方法。尽管单例模式通过将构造函数私有化来防止外部创建对象(这是单例模式的核心),反射仍能绕过这一限制。通过调用私有构造函数,反射可以实例化新的对象,破坏单例模式的唯一性。

2.1 反射破坏单例的示例

以下代码展示了如何通过反射破坏单例模式:

import java.lang.reflect.Constructor;public class ReflectionSingletonTest {public static void main(String[] args) {try {// 获取单例实例Singleton instance1 = Singleton.getInstance();// 通过反射获取私有构造方法Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();constructor.setAccessible(true);  // 设置可访问性// 通过反射创建新的实例Singleton instance2 = constructor.newInstance();// 比较两个实例是否相同System.out.println("instance1 == instance2: " + (instance1 == instance2));} catch (Exception e) {e.printStackTrace();}}
}

输出结果

instance1 == instance2: false

通过反射,我们可以调用私有构造函数创建新的对象,导致 instance1instance2 是两个不同的对象,从而破坏了单例模式。


3. 如何防止反射破坏单例模式

为了防止反射破坏单例模式,我们可以在构造方法中添加逻辑判断,确保即使通过反射调用,实例也不能被多次创建。具体做法是在构造方法中检查实例是否已经存在,如果存在则抛出异常。

3.1 防御措施示例

以下是修改后的单例类,通过在构造方法中加入防御机制,防止反射破坏:

public class Singleton {private static final Singleton INSTANCE = new Singleton();private Singleton() {// 防止反射创建新实例if (INSTANCE != null) {throw new RuntimeException("单例模式禁止反射创建实例!");}}public static Singleton getInstance() {return INSTANCE;}
}
3.2 改进后的测试结果

当使用反射尝试创建新实例时,将会抛出异常:

Exception in thread "main" java.lang.RuntimeException: 单例模式禁止反射创建实例!

这种防御机制有效地阻止了反射破坏单例模式。


4. 总结

单例模式是 Java 中非常重要的设计模式,它确保了类只有一个实例,但由于 Java 的反射机制,饿汉式、懒汉式、双重校验锁和静态内部类实现的单例模式都可以被反射轻松破坏。通过在构造方法中添加实例校验机制,我们可以有效防止反射创建多个实例。

  • 饿汉式:即使不使用,也会提前加载单例,浪费内存。
  • 懒汉式:通过 synchronized 实现线程安全的懒加载,但并发性能较低。
  • 双重校验锁:提高了并发性能,使用 volatile 防止指令重排问题。
  • 静态内部类:相较于双重校验锁更加优雅,实现了懒加载和线程安全。

在实际开发中,我们需要根据具体需求选择合适的单例模式,并针对可能的反射攻击采取相应的防御措施。
除了反射会对单例模式进行破坏,序列化也会如此。具体可以看这篇文章:序列化对于单例模式的破坏

这篇关于单例模式以及反射对单例模式的破坏及防御的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1147360

相关文章

在JS中的设计模式的单例模式、策略模式、代理模式、原型模式浅讲

1. 单例模式(Singleton Pattern) 确保一个类只有一个实例,并提供一个全局访问点。 示例代码: class Singleton {constructor() {if (Singleton.instance) {return Singleton.instance;}Singleton.instance = this;this.data = [];}addData(value)

模版方法模式template method

学习笔记,原文链接 https://refactoringguru.cn/design-patterns/template-method 超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。 上层接口有默认实现的方法和子类需要自己实现的方法

【iOS】MVC模式

MVC模式 MVC模式MVC模式demo MVC模式 MVC模式全称为model(模型)view(视图)controller(控制器),他分为三个不同的层分别负责不同的职责。 View:该层用于存放视图,该层中我们可以对页面及控件进行布局。Model:模型一般都拥有很好的可复用性,在该层中,我们可以统一管理一些数据。Controlller:该层充当一个CPU的功能,即该应用程序

迭代器模式iterator

学习笔记,原文链接 https://refactoringguru.cn/design-patterns/iterator 不暴露集合底层表现形式 (列表、 栈和树等) 的情况下遍历集合中所有的元素

《x86汇编语言:从实模式到保护模式》视频来了

《x86汇编语言:从实模式到保护模式》视频来了 很多朋友留言,说我的专栏《x86汇编语言:从实模式到保护模式》写得很详细,还有的朋友希望我能写得更细,最好是覆盖全书的所有章节。 毕竟我不是作者,只有作者的解读才是最权威的。 当初我学习这本书的时候,只能靠自己摸索,网上搜不到什么好资源。 如果你正在学这本书或者汇编语言,那你有福气了。 本书作者李忠老师,以此书为蓝本,录制了全套视频。 试

利用命令模式构建高效的手游后端架构

在现代手游开发中,后端架构的设计对于支持高并发、快速迭代和复杂游戏逻辑至关重要。命令模式作为一种行为设计模式,可以有效地解耦请求的发起者与接收者,提升系统的可维护性和扩展性。本文将深入探讨如何利用命令模式构建一个强大且灵活的手游后端架构。 1. 命令模式的概念与优势 命令模式通过将请求封装为对象,使得请求的发起者和接收者之间的耦合度降低。这种模式的主要优势包括: 解耦请求发起者与处理者

springboot实战学习(1)(开发模式与环境)

目录 一、实战学习的引言 (1)前后端的大致学习模块 (2)后端 (3)前端 二、开发模式 一、实战学习的引言 (1)前后端的大致学习模块 (2)后端 Validation:做参数校验Mybatis:做数据库的操作Redis:做缓存Junit:单元测试项目部署:springboot项目部署相关的知识 (3)前端 Vite:Vue项目的脚手架Router:路由Pina:状态管理Eleme

状态模式state

学习笔记,原文链接 https://refactoringguru.cn/design-patterns/state 在一个对象的内部状态变化时改变其行为, 使其看上去就像改变了自身所属的类一样。 在状态模式中,player.getState()获取的是player的当前状态,通常是一个实现了状态接口的对象。 onPlay()是状态模式中定义的一个方法,不同状态下(例如“正在播放”、“暂停

软件架构模式:5 分钟阅读

原文: https://orkhanscience.medium.com/software-architecture-patterns-5-mins-read-e9e3c8eb47d2 软件架构模式:5 分钟阅读 当有人潜入软件工程世界时,有一天他需要学习软件架构模式的基础知识。当我刚接触编码时,我不知道从哪里获得简要介绍现有架构模式的资源,这样它就不会太详细和混乱,而是非常抽象和易

使用Spring Boot集成Spring Data JPA和单例模式构建库存管理系统

引言 在企业级应用开发中,数据库操作是非常重要的一环。Spring Data JPA提供了一种简化的方式来进行数据库交互,它使得开发者无需编写复杂的JPA代码就可以完成常见的CRUD操作。此外,设计模式如单例模式可以帮助我们更好地管理和控制对象的创建过程,从而提高系统的性能和可维护性。本文将展示如何结合Spring Boot、Spring Data JPA以及单例模式来构建一个基本的库存管理系统