1.单例优缺点
单例模式是一种常用的设计模式,它确保一个类仅有一个实例,并提供一个全局访问点。单例模式的使用具有一定的优点,同时也伴随着一些潜在的缺点。
优点
- 资源控制:单例模式能够确保资源如数据库连接或文件系统的一致访问,防止资源冲突。
- 内存占用减少:由于实例唯一,单例模式帮助减少系统内存的使用,避免重复对象的创建和销毁。
- 全局访问点:提供了一个访问唯一实例的全局访问点,方便从系统任何其他代码位置访问该实例。
- 数据共享:单例实例作为全局实例,便于在不同的程序模块间共享数据。
- 延迟初始化:单例模式支持延迟初始化(懒汉式),仅在需要时创建实例,节省资源和减少启动时间。
缺点
- 违背单一职责原则:单例类除了管理自身的实例外,还承担了业务逻辑的职责,违背了单一职责原则。
- 全局状态引起的问题:单例模式在多线程环境下,如果不正确处理,会引入全局状态的问题,如实例在多线程间共享时可能出现同步问题。
- 对测试的影响:单例的全局状态使得单元测试变得困难,尤其是当单例类不遵循接口时,很难被Mock替代。
- 扩展困难:由于单例模式的实例是全局唯一的,扩展和修改单例类的功能可能会影响到所有使用该实例的地方。
- 隐藏依赖:单例模式的使用可能会隐藏类之间的依赖关系,而不是通过接口或构造函数注入,这降低了代码的可读性和可维护性。
结论
单例模式在需要控制资源访问或确保全局唯一性时非常有用,但是它也带来了一些设计上的挑战,尤其是在大型、复杂系统中。使用单例模式时应该仔细考虑其对系统设计的影响,尽可能地遵循设计原则,如依赖倒置原则和接口隔离原则,以保持代码的灵活性和可测试性。在某些情况下,考虑使用依赖注入框架来管理单例实例,可能是更好的选择。
2.单例模式使用注意事项
选择合适的单例模式创建方式依赖于应用的需求、资源的特性以及环境的限制。下面是几种常见的单例创建方式以及它们各自的适用场景:
1. 懒汉式(线程不安全)
- 特点:实例在首次使用时创建。
- 适用场景:单线程环境,或者实例创建开销小且在程序生命周期中很早就会被用到。
2. 懒汉式(线程安全)
- 特点:实例在首次使用时创建,通过同步机制确保线程安全。
- 适用场景:多线程环境,但对性能要求不是特别高,因为同步操作只需要在实例创建时执行一次,之后获取实例不再需要同步。
3. 饿汉式
- 特点:类加载时就完成了实例的初始化,基于类加载机制避免了多线程的同步问题。
- 适用场景:单例对象占用资源少,按需加载不是必须的场景,或者确保实例的创建不依赖于参数和配置设置。
4. 双重检查锁定(Double-Checked Locking)
- 特点:在实例未初始化时才同步,避免了每次访问时的同步开销。
- 适用场景:多线程环境中,需要延迟实例化并且确保实例只被创建一次。
5. 静态内部类
- 特点:利用类加载机制保证初始化实例时只有一个线程。
- 适用场景:既需要延迟加载,又要确保线程安全。静态内部类方式在很多情况下是最推荐的单例实现方式,它既能保证延迟加载,又能保证高效的线程安全。
6. 枚举方式
- 特点:最简单的实现方式,通过Java枚举类型本身的特性,保证实例创建的安全性和唯一性。
- 适用场景:实现单例的最佳方法之一。不仅能避免多线程同步问题,还能防止反序列化重新创建新的对象。
总结
- 对于大部分多线程场景,双重检查锁定和静态内部类方式是较好的选择,它们既保证了懒加载,又保证了高效的线程安全。
- 如果单例类的实例在应用启动时就必须被使用,或者实例的创建开销不重要,可以选择饿汉式。
- 如果需要绝对的序列化安全,可以使用枚举方式实现单例。
在选择单例模式的实现方式时,应该根据实际需求和具体情况做出合理的决策。
3.单例防止反射漏洞攻击
在Java中,反射可以用来绕过私有构造函数的限制,从而创建多个单例对象的实例,这违反了单例模式的原则。为了防止这种反射攻击,可以采取一些措施确保单例类的安全性。
1. 抛出异常
在单例类的私有构造方法中检查实例是否已经存在,如果存在,则抛出一个异常。这样可以阻止通过反射创建第二个实例。
public class Singleton {private static Singleton instance;private Singleton() {if (instance != null) {throw new IllegalStateException("Instance already exists!");}}public static Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}
}
2. 使用枚举实现单例
Java枚举类型(enum
)提供了一种简单的方式来实现单例模式,而且枚举类型天然防止了通过反射和序列化攻击的可能性。
public enum Singleton {INSTANCE;public void someMethod() {// 方法实现}
}
在这种方法中,Singleton.INSTANCE
总是返回相同的实例,JVM保证枚举值是全局唯一的。此外,Java禁止通过反射来创建枚举实例,这自然防止了通过反射创建新实例的漏洞。
3. 使用内部静态类
虽然使用内部静态类本身不能防止反射攻击,但结合私有构造方法内的安全检查,可以提供双重保障。
public class Singleton {private Singleton() {if (SingletonHolder.INSTANCE != null) {throw new IllegalStateException("Instance already exists!");}}private static class SingletonHolder {private static final Singleton INSTANCE = new Singleton();}public static Singleton getInstance() {return SingletonHolder.INSTANCE;}
}
总结
- 尽管可以通过在私有构造函数中添加检查来阻止反射创建第二个实例,但这种方法并非完美。它可能导致反射调用的代码抛出异常。
- 使用枚举类型是防止反射攻击最简单也是最推荐的方法。枚举类型的单例不仅能避免多线程同步问题,还能防止反射和序列化攻击。
- 应该注意的是,设计和实现单例时,除了考虑防止反射攻击外,还应该综合考虑其他安全性因素,如线程安全和防止序列化攻击。