1. 概念
单例模式(Singleton Pattern)是一种常用的软件设计模式,它属于创建型模式之一。该模式的主要目的是确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。
2. 特点
- 私有构造函数:防止其他对象实例化该类。
- 静态内部成员变量:用来保存该类的唯一实例。
- 公共静态方法:提供一个全局访问点来获取该唯一实例。
3. 优缺点
优点:
- 控制对单一实例的访问;
- 全局唯一性保证;
- 减少系统资源消耗。
缺点:
- 违反单一职责原则;
- 增加系统复杂度和调试难度;
- 可能导致程序难以扩展。
4. 使用场景
- 需要频繁实例化的对象且消耗大量资源,如数据库连接池、线程池等。
- 需要控制实例数量并允许外界能够访问的情况,比如配置管理器等。
5. 单例模式的五种实现方式
5.1. 饿汉式单例
饿汉式单例模式的特点
- 静态常量:在类定义时就已经实例化了一个静态成员变量。
- 线程安全:由于实例化发生在类初始化阶段,因此不需要额外的同步措施来保证线程安全。
- 立即加载:无论是否使用该单例对象,都会在JVM启动时完成实例化。
代码示例
public class HungrySingleton {
private static final HungrySingleton INSTANCE = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return INSTANCE;
}
}
饿汉式单例模式的优点:
- 线程安全:由于实例化是在类加载时完成的,所以不需要担心多线程问题。
- 简单易用:实现起来非常简单,不需要额外的同步处理。
- 避免懒加载的问题:不会出现因为懒加载而导致的性能问题或者多线程下的并发问题。
饿汉式单例模式的缺点:
- 内存占用:即使应用程序在运行过程中从未使用到这个单例,也会在类加载时占用内存。
- 延迟加载问题:如果应用程序很大,有很多单例类,那么在启动时会加载很多不必要的类,影响启动速度。
- 不适用于动态代理:如果需要通过动态代理等方式增强单例的行为,则饿汉式单例模式不是最佳选择。
使用场景:
- 当你需要确保在整个应用生命周期中只存在一个实例,并且这个实例在应用启动时就需要准备好时。
- 对于那些资源消耗不大、但要求线程安全且无需延迟加载的场景特别适用。
总结来说,饿汉式单例模式适合于那些不需要延迟加载、资源占用不大且需要保证线程安全的场景。如果你的应用程序对资源的消耗比较敏感,或者希望延迟加载单例直到真正需要时再创建,那么可以考虑使用懒汉式单例模式或其他变种。
5.2 懒汉式单例
懒汉式单例模式的特点
- 延迟实例化:在首次使用时才创建实例。
- 按需加载:节省资源,只有在确实需要的时候才会创建实例。
- 线程安全问题:如果不采取措施,可能会存在线程安全问题。
代码示例
public class LazySingleton {
private static volatile LazySingleton instance;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
}
懒汉式单例模式的优点:
- 节省资源:只在需要时创建实例,减少了不必要的内存占用。
- 延迟加载:可以提高程序启动的速度,因为实例化操作被推迟到了真正需要的时候。
- 灵活性:可以通过子类重写构造方法或者在实例化过程中执行特定的初始化操作。
懒汉式单例模式的缺点:
- 线程安全问题:如果没有正确处理,可能会导致多个线程同时创建多个实例。
- 效率问题:每次调用 getInstance() 方法时都需要检查实例是否存在,增加了判断的开销。
- 实现复杂度:为了保证线程安全,通常需要使用同步机制,这会增加实现的复杂度。
使用场景:
- 当你希望节省资源,只在真正需要的时候创建单例对象。
- 当单例对象的创建过程比较耗时或耗资源时。
- 当你需要在单例对象创建时进行一些初始化工作。
注意事项:
- 如果在多线程环境中使用懒汉式单例,必须确保线程安全。可以采用双重检查锁定(Double-Checked Locking, DCL)来实现。
- 如果使用DCL来实现线程安全的懒汉式单例,需要注意 volatile 关键字的使用,以避免指令重排导致的问题。
总的来说,懒汉式单例模式适合于那些需要节省资源、希望延迟加载、并且对线程安全性有要求的场景。如果不确定是否需要延迟加载,或者对资源的消耗不是很敏感,那么可以考虑使用饿汉式单例模式。
5.3 枚举式单例
枚举式单例模式是利用Java中的枚举类型来实现单例模式的一种方式。这种方法被认为是实现单例模式最简单、最安全的方式之一。
枚举式单例模式的特点:
- 自动支持序列化机制:枚举类型的序列化机制保证了单例对象的唯一性。
- 线程安全:枚举类型的构造函数默认就是线程安全的。
- 简洁高效:不需要额外的同步机制或复杂的逻辑。
代码示例
public enum EnumSingleton {
INSTANCE;
}
枚举式单例模式的优点:
- 线程安全:枚举类型的构造函数默认就是线程安全的,因此不需要额外的同步处理。
- 序列化安全:枚举类型天然支持序列化,不会因为反序列化而导致多个实例。
- 简洁性:代码结构非常简单,易于理解和维护。
- 延迟加载:虽然枚举类型会在类加载时创建实例,但由于枚举类型是惰性加载的,因此枚举的初始化会等到首次访问时才发生。
- 可扩展性:可以在枚举中定义多个实例,从而支持更灵活的设计。
枚举式单例模式的缺点:
- 可读性:对于不熟悉枚举特性的开发者来说,可能不容易理解为什么枚举可以实现单例。
- 非传统:传统的单例模式实现通常通过私有构造函数和静态方法实现,枚举式单例可能与传统做法不同,某些开发者可能不太习惯。
- 功能限制:枚举类型本身具有一些限制,例如不能继承其他类,只能实现接口。
使用场景:
- 当你希望实现一个简单、高效且线程安全的单例模式时。
- 当你的单例对象不需要继承其他类时。
- 当你希望避免序列化问题带来的额外开销时。
注意事项:
- 枚举类型的实例是自动序列化的,因此不需要实现 Serializable 接口。
- 枚举类型的构造函数默认是私有的,因此不需要显式声明私有构造函数。
- 枚举类型的实例默认是唯一的,因此不需要额外的方法来获取实例。
总的来说,枚举式单例模式是一种简单而强大的实现方式,它非常适合那些需要线程安全、序列化安全以及简洁实现的需求场景。如果你的应用程序中有一个不需要继承其他类的单例对象,并且需要确保线程安全和序列化安全,那么枚举式单例模式是一个很好的选择。
5.4 容器式单例
示例代码
public class ContainerSingleton {
private ContainerSingleton() {
}
private static Map<String, Object> ioc = new ConcurrentHashMap<>();
public static Object getInstance(String className) {
if (ioc.containsKey(className)) {
return ioc.get(className);
} else {
Object instance = null;
try {
instance = Class.forName(className).newInstance();
ioc.put(className, instance);
} catch (Exception e) {
e.printStackTrace();
}
return instance;
}
}
}
5.5 ThreadLocal单例
ThreadLocal单例模式的特点:
- 线程隔离:每个线程都有自己的独立实例,互不干扰。
- 懒加载:实例在第一次使用时创建,避免了不必要的资源消耗。
- 生命周期管理:实例的生命周期与线程的生命周期相同,线程结束后,实例也会被回收。
代码示例
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> instance = ThreadLocal.withInitial(ThreadLocalSingleton::new);
private ThreadLocalSingleton() {
}
public static ThreadLocalSingleton getInstance() {
return instance.get();
}
}
ThreadLocal单例模式的优点
- 避免共享资源竞争:每个线程都有自己的实例,避免了多线程环境下的竞争条件。
- 简化线程安全管理:不需要显式的同步机制,降低了复杂性。
- 提高性能:由于没有锁的开销,性能更高。
ThreadLocal单例模式的缺点
- 内存泄漏风险:如果ThreadLocal的引用不被清理,可能导致内存泄漏。
- 调试困难:由于每个线程都有独立的实例,调试时可能很难追踪状态。
- 不适合长生命周期的线程:对于长时间运行的线程,可能导致内存占用增加。
使用场景
- 用户会话管理:在Web应用中,每个用户的会话信息可以使用ThreadLocal存储。
- 数据库连接:在每个线程中存储数据库连接,避免连接竞争。
- 日志上下文:为每个线程提供独立的日志上下文,便于记录线程相关信息。
注意事项
- 及时清理:在线程结束时,确保调用ThreadLocal.remove()来清除引用。
- 避免使用静态变量:避免在ThreadLocal中使用静态变量,以防止内存泄漏。
- 合理使用:仅在必要时使用ThreadLocal,过度使用可能导致复杂性增加。
6. 破坏单例
6.1 反射
防止反射破坏方案:
private PreventDamageSingleton() {
if (INSTANCE != null) {
throw new IllegalStateException("单例对象已存在,请使用getInstance()方法获取实例。");
}
}
反射破坏单例原理:
try {
// 通过反射获取构造函数
Constructor<HungrySingleton> constructor = HungrySingleton.class.getDeclaredConstructor();
// 允许访问私有构造函数
constructor.setAccessible(true);
// 创建实例
HungrySingleton instance = constructor.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
6.2 序列化与反序列化
序列化:把内存中对象的状态转换为字节码的形式,把字节码通过IO输出流写到磁盘上,永久保存下来,持久化。
反序列化:将持久化的字节码内容,通过IO输入流读到内存中来,转化成一个Java对象。
因为序列化那里使用ObjectInputStream.java里面有判断是否有readResolve方法,如果有最终返回的这个obj就是利用反射调用readResolve方法拿到的实例对象赋值给了obj然后返回。
**防止序列化破坏方案:**单例类中写readResolve方法直接返回单例对象
private Object readResolve() {
return INSTANCE;
}
反序列化破坏单例原理
handles.finish(passHandle);
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}
6.3 总结防止破坏单例
以饿汉式单例举例,其他的单例也是一样道理
public class PreventDamageSingleton {
private static final long serialVersionUID = 1L;
private static final PreventDamageSingleton INSTANCE = new PreventDamageSingleton();
private PreventDamageSingleton() {
// 防止反射破坏单例
if (INSTANCE != null) {
throw new IllegalStateException("单例对象已存在,请使用getInstance()方法获取实例。");
}
}
public static PreventDamageSingleton getInstance() {
return INSTANCE;
}
// 反序列化保护
protected Object readResolve() {
return INSTANCE;
}
}