【Java EE】单例模式

单例模式

单例模式(Singleton Pattern)是最简单、最常用的创建型模式之一。

什么是单例模式?

单例模式确保一个类在整个应用程序生命周期中只有一个实例,并提供一个全局访问点。这种设计模式在以下场景特别适用:

  • 数据库连接池
  • 日志记录器
  • 配置管理类
  • 线程池
  • 缓存管理器

单例模式的实现方式

1. 饿汉模式 (Eager Initialization)

java 复制代码
class Singleton {
    // 1. 静态实例
    private static Singleton instance = new Singleton();

    // 2. 必须私有化构造方法!否则外部还是可以 new 出来,单例就失效了
    private Singleton() {}

    // 3. 对外提供接口
    public static Singleton getInstance() {
        return instance;
    }
}

代码中有三个关键点决定了它是单例:

  • private static Singleton instance = new Singleton();static关键词意味着这个变量属于类,而不属于某个具体的对象,它在类被加载时就会初始化。类一加载,对象就创建好了。(立即实例化
  • public static Singleton getInstance(): 这是外部获取该实例的唯一通道。
  • private Singleton() {} :在标准的单例模式中,必须有一个私有构造方法,防止外部通过 new Singleton() 再次创建对象。(只有一个实例

Q:为什么叫饿汉模式?

这个名字很形象。它就像一个饿坏了的人,不管你现在需不需要这个对象,他在类加载阶段就把对象"吃"进内存创建好了。

饿汉模式优点

  1. 线程安全 :这是它最大的优势。Java 虚拟机(JVM)在加载类时,是天然线程安全的。在多线程环境下,绝对不会出现创建出两个 instance 的情况。
  2. 执行效率高 :获取实例时不需要任何 synchronized 锁或条件判断,直接返回已经创建好的对象。

饿汉模式缺点

  1. 资源浪费(内存占用):如果这个单例对象的创建非常耗费资源,而程序从始至终都没用到这个类,那么这个对象依然会常驻内存,白白占用空间。
  2. 不支持延迟加载:与懒汉模式(需要时再创建)相反。

2.懒汉模式(Lazy Initialization)

java 复制代码
class SingletonLazy {
	// 1. 静态实例
    private static SingletonLazy instance = null;

    // 2. 私有构造方法,防止外部 new 实例
    private SingletonLazy() {}

    public static SingletonLazy getInstance() {
        // 3. 第一重检查:如果实例已经创建,直接返回,避免不必要的锁竞争
        if (instance == null) {
            // 4. 加锁,确保只有一个线程能进入创建逻辑
            synchronized (SingletonLazy.class) {
                // 5. 第二重检查:抢到锁后再次确认实例是否被其他线程先一步创建
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
}

代码中有三个关键点决定了它是单例:

  • private static SingletonLazy instance = null; 初始状态下,只是声明了一个引用,并没有创建真正的对象。这节省了程序启动时的内存开销。

  • private SingletonLazy() {} :在标准的单例模式中,必须有一个私有构造方法,防止外部通过 new Singleton() 再次创建对象。(只有一个实例

为什么要加锁?------ 懒汉式单例的线程安全问题⭐

没有加锁的代码如下:

java 复制代码
class SingletonLazy {
    // 1. 静态实例
    private static SingletonLazy instance = null;

    // 2. 私有构造方法,防止外部 new 实例
    private SingletonLazy() {}

    public static SingletonLazy getInstance() {
        if (instance == null) {           // ① 第一次检查
            instance = new SingletonLazy(); // ② 创建实例
        }
        return instance;
    }
}

在多线程环境下,线程 1和线程 2 可能同时进入if (instance == null)判断,导致 new 两次,破坏了单例原则。

加锁解决方案

方案一:同步方法(简单但性能稍差)

java 复制代码
public static synchronized SingletonLazy getInstance() {
    if (instance == null) {
        instance = new SingletonLazy();
    }
    return instance;
}

synchronized 保证同一时刻只有一个线程能执行该方法,其他线程必须等待。

方案二:双重检查锁(高性能推荐)

java 复制代码
public static SingletonLazy getInstance() {
    if (instance == null) {               // 第一次检查(无锁)
        synchronized (SingletonLazy.class) {
            if (instance == null) {       // 第二次检查(有锁)
                instance = new SingletonLazy();
            }
        }
    }
    return instance;
}

双重 if 的作用------ 懒汉式单例的效率提升⭐

  • 外层 if :是为了效率 。避免每次调用 getInstance() 都加锁,当实例已经创建完成后,后续所有线程直接跳过同步代码块,快速返回实例。
  • 内层 if :是为了安全。防止多个线程在外层判断通过后,重复创建实例。

3. 双重检查锁(DCL)(懒汉模式Plus)

双重检查锁(Double-Checked Locking,DCL)

java 复制代码
class SingletonLazy {
    // 1. 使用 volatile 关键字,禁止指令重排序,确保多线程下的可见性
    private static volatile SingletonLazy instance = null;

    // 2. 私有构造方法,防止外部 new 实例
    private SingletonLazy() {}

    public static SingletonLazy getInstance() {
        // 3. 第一重检查:如果实例已经创建,直接返回,避免不必要的锁竞争
        if (instance == null) {
            // 4. 加锁,确保只有一个线程能进入创建逻辑
            synchronized (SingletonLazy.class) {
                // 5. 第二重检查:抢到锁后再次确认实例是否被其他线程先一步创建
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
}

要让单例模式的懒汉模式 (Lazy Initialization)真正达到工业级的标准,代码中必须解决两个核心问题:原子性指令重排序

为什么必须加 volatile?------ 懒汉式单例中线程安全问题⭐

instance = new SingletonLazy(); 这行代码在 CPU 层面其实分为三步:

  1. 分配内存空间。
  2. 初始化对象(执行构造函数)。
  3. 将引用指向内存空间。

如果没有 volatile,编译器可能会为了优化而进行指令重排序 ,变成 1 -> 3 -> 2

如果执行到 3 时(对象还没初始化完),另一个线程来了,执行了最外层的 if (instance == null)。它会发现 instance 不为 null,于是直接拿走了一个还没初始化完成的对象,导致程序报错。

4. 静态内部类(懒汉模式Pro Max)

利用 JVM 的类加载机制保证线程安全:

java 复制代码
class Singleton {
    private Singleton() {}

    // 只有在调用 getInstance() 时,内部类 InnerClass 才会加载
    // 加载过程中 JVM 会保证线程安全,同时实现了延迟初始化
    private static class InnerClass {
        private static final Singleton INSTANCE = new Singleton();
    }

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

这是最优雅的实现方式,既实现了懒加载,又保证了线程安全。

单例模式的缺陷

集群环境------伪单例

在分布式部署中,每个 JVM 实例都有自己的单例,造成伪单例:

java 复制代码
// 这不是真正的全局单例!
public class UserSessionManager {
    // 在集群中,每个节点都有自己的实例
    private static UserSessionManager instance;
}
  • 使用分布式缓存(如 Redis)
  • 使用数据库存储共享状态
  • 使用应用服务器的分布式会话管理

序列化------破坏单例

java 复制代码
public class SerializedSingleton implements Serializable {
    private static final long serialVersionUID = 1L;
    private static final SerializedSingleton INSTANCE = new SerializedSingleton();
    
    private SerializedSingleton() {}
    
    public static SerializedSingleton getInstance() {
        return INSTANCE;
    }
    
    // 防止反序列化创建新实例
    protected Object readResolve() {
        return INSTANCE;
    }
}

反射------破坏单例

反射可以调用私有构造器创建新实例:

java 复制代码
Constructor<Logger> constructor = Logger.class.getDeclaredConstructor();
constructor.setAccessible(true);
Logger newInstance = constructor.newInstance(); // 破坏了单例!

防御措施

java 复制代码
public class SecureSingleton {
    private static final SecureSingleton INSTANCE = new SecureSingleton();
    private static boolean initialized = false;
    
    private SecureSingleton() {
        if (initialized) {
            throw new RuntimeException("单例模式被破坏");
        }
        initialized = true;
    }
    
    public static SecureSingleton getInstance() {
        return INSTANCE;
    }
}

笔试面试题

写一个单例模式

java 复制代码
class Singleton {
    // 1. 静态实例
    private static Singleton instance = new Singleton();

    // 2. 必须私有化构造方法!否则外部还是可以 new 出来,单例就失效了
    private Singleton() {}

    // 3. 对外提供接口
    public static Singleton getInstance() {
        return instance;
    }
}
java 复制代码
class SingletonLazy {
    // 1. 使用 volatile 关键字,禁止指令重排序,确保多线程下的可见性
    private static volatile SingletonLazy instance = null;

    // 2. 私有构造方法,防止外部 new 实例
    private SingletonLazy() {}

    public static SingletonLazy getInstance() {
        // 3. 第一重检查:如果实例已经创建,直接返回,避免不必要的锁竞争
        if (instance == null) {
            // 4. 加锁,确保只有一个线程能进入创建逻辑
            synchronized (SingletonLazy.class) {
                // 5. 第二重检查:抢到锁后再次确认实例是否被其他线程先一步创建
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
}

饿汉式和懒汉式有什么区别?

对比项 饿汉式 懒汉式
创建时机 类加载时 首次调用时
线程安全 ✅ 天然安全 ❌ 不安全
内存占用 可能浪费 按需分配
实现复杂度 简单 需要加锁

懒汉式怎么解决线程安全问题?

synchronized 关键字 + volatile 关键字

DCL为什么要两次判空?

java 复制代码
public static Singleton getInstance() {
    if (instance == null) {          // 第一次检查:为了效率
        synchronized (Singleton.class) {
            if (instance == null) {  // 第二次检查:为了安全
                instance = new Singleton();
            }
        }
    }
    return instance;
}
  • 外层 if:避免每次调用都加锁。实例创建后,99.9% 的调用直接返回,无需竞争锁
  • 内层 if:防止多个线程同时通过外层判断后重复创建。当 T1 创建完实例释放锁,T2 拿到锁后如果不再次判断,就会再 new 一个,破坏单例

DCL 为什么要加 volatile?

instance = new Singleton() 不是原子操作,JVM 会分三步:

  1. 分配内存空间
  2. 初始化对象
  3. 将引用指向内存地址

指令重排序可能导致步骤 3 先于步骤 2 执行。此时线程 T1 刚分配内存并赋值引用,实例还未初始化,线程 T2 进来发现 instance != null,直接返回一个未初始化完成的对象,程序崩溃。

volatile 通过内存屏障禁止指令重排序,保证对象完全初始化后才赋值引用。

单例模式可能造成什么问题?

内存泄漏陷阱

java 复制代码
// ❌ 错误用法------Activity 无法被回收
public class Singleton {
    private Context mContext;
    private Singleton(Context context) {
        this.mContext = context;  // 持有 Activity 引用
    }
}

// ✅ 正确用法
mContext = context.getApplicationContext();  // 用 ApplicationContext

集群环境问题:分布式部署中,每个 JVM 都有自己的单例,需要通过 Redis 等集中式缓存实现真正的全局单例。

静态内部类方式和 DCL 哪个更好?为什么?

《Java并发编程实战》推荐静态内部类方式优于 DCL。

java 复制代码
public class Singleton {
    private Singleton() {}
    
    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}
  • 利用 JVM 类加载机制保证线程安全,无需显式同步
  • 实现更简洁,没有 volatile 和锁的开销
  • 天然支持延迟加载(Holder 类只在第一次调用时加载)

枚举单例为什么被认为是最完美的单例?

  1. 天然线程安全:JVM 保证枚举实例唯一
  2. 防反射攻击:反射无法创建枚举实例
  3. 防序列化破坏:枚举的序列化机制保证返回同一实例,无需额外代码

DCL、静态内部类方式和枚举的选择

DCL 方式的 volatile 虽然解决了指令重排序问题,但有一定性能开销。在实际项目中,如果不需要延迟加载,优先选择静态内部类方式;如果需要防止反射和序列化攻击,枚举是更好的选择。具体选型要看业务场景。

相关推荐
志飞2 小时前
springboot配置可持久化本地缓存ehcache
java·spring boot·缓存·ehcache·ehcache持久化
itzixiao2 小时前
L1-051 打折(5分)[java][python]
java·python·算法
それども2 小时前
Spring Bean 注入的优先级顺序
java·数据库·sql·spring
ID_180079054732 小时前
Python 实现京东商品详情 API 数据准确性校验(极简可直接用)
java·前端·python
贾斯汀玛尔斯2 小时前
每天学一个算法--Aho–Corasick 自动机
java·linux·算法
LF男男2 小时前
Action- C# 内置的委托类型
java·开发语言·c#
练习时长一年3 小时前
@NotEmpty注解引发的报错
java·服务器·前端
西海天际蔚蓝3 小时前
用AI写的一个包含web和小程序的个人简历
java
郝学胜-神的一滴3 小时前
[力扣 227] 双栈妙解表达式计算:从思维逻辑到C++实战,吃透反向波兰式底层原理
java·前端·数据结构·c++·算法