Java案例怎么实现单例模式?

wen java案例 11

Java单例模式深度解析:从基础实现到高级优化(附完整案例)

目录导读

  1. 单例模式核心概念 – 什么是单例?为什么要用单例?
  2. Java单例模式的七种经典实现 – 饿汉式、懒汉式、双重检查锁、静态内部类、枚举、容器式、ThreadLocal
  3. 实战案例对比 – 不同场景下如何选择最优方案
  4. 常见陷阱与面试高频问题
  5. 性能测试与安全加固
  6. Q&A 问答精选

单例模式核心概念

单例模式(Singleton Pattern) 是 Java 23 种经典设计模式中最简单也最容易被问倒的模式,它的核心目标:确保一个类在 JVM 中只有一个实例,并提供一个全局访问点

Java案例怎么实现单例模式?

为什么需要单例?

  • 资源节省:数据库连接池、线程池、缓存等对象创建成本高,只保留一个实例避免重复初始化。
  • 状态统一:配置文件读取、日志记录器、计数器等需要全局一致的访问。
  • 线程安全:避免多线程环境下实例不一致的问题。

单例必须满足的条件:

  1. 私有构造函数(防止外部 new)
  2. 私有静态的实例变量
  3. 公有的静态获取方法

问:单例模式一定线程安全吗? 答:不一定,懒汉式不加锁就是线程不安全的,不同实现方式对线程安全的支持程度不同。


Java单例模式的七种经典实现

1 饿汉式(Eager Initialization)

public class SingletonEager {
    private static final SingletonEager instance = new SingletonEager();
    private SingletonEager() {}
    public static SingletonEager getInstance() { return instance; }
}

优点:线程安全(类加载时就创建了实例),实现简单。
缺点:无法延迟加载,如果实例创建开销大且未使用会造成资源浪费。

2 懒汉式(Lazy Initialization)

public class SingletonLazy {
    private static SingletonLazy instance;
    private SingletonLazy() {}
    public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy(); // 非线程安全
        }
        return instance;
    }
}

问题:多线程环境下可能创建多个实例,需要加 synchronized 修饰方法。

3 双重检查锁(Double-Checked Locking)

public class SingletonDCL {
    private static volatile SingletonDCL instance; // volatile 禁止指令重排序
    private SingletonDCL() {}
    public static SingletonDCL getInstance() {
        if (instance == null) {
            synchronized (SingletonDCL.class) {
                if (instance == null) {
                    instance = new SingletonDCL();
                }
            }
        }
        return instance;
    }
}

关键点volatile 关键字确保 instance = new SingletonDCL() 的写操作不被重排序。
适用场景:高并发环境且需要延迟加载时。

4 静态内部类(Static Inner Class)

public class SingletonHolder {
    private SingletonHolder() {}
    private static class Holder {
        private static final SingletonHolder INSTANCE = new SingletonHolder();
    }
    public static SingletonHolder getInstance() {
        return Holder.INSTANCE;
    }
}

原理:JVM 在类初始化阶段保证线程安全,且内部类只有在 getInstance() 被调用时才加载。
推荐:兼具延迟加载和线程安全,且无锁开销。

5 枚举方式(Enum Singleton)

public enum SingletonEnum {
    INSTANCE;
    public void doSomething() { System.out.println("枚举单例"); }
}

最强方案:自动防止反射攻击和序列化破坏,是《Effective Java》作者Joshua Bloch 推荐的方式。

6 容器式(Map管理)

public class SingletonManager {
    private static Map<String, Object> map = new HashMap<>();
    static {
        map.put("config", new ConfigManager());
    }
    public static Object getInstance(String key) {
        return map.get(key);
    }
}

适用:管理多个单例对象时。

7 ThreadLocal 单例

public class SingletonThreadLocal {
    private static ThreadLocal<SingletonThreadLocal> tl = ThreadLocal.withInitial(() -> new SingletonThreadLocal());
    private SingletonThreadLocal() {}
    public static SingletonThreadLocal getInstance() {
        return tl.get();
    }
}

特点:每个线程有自己的单例实例,并非全局唯一。


实战案例对比:最适合你的场景是什么?

实现方式 延迟加载 线程安全 反反射攻击 序列化安全 推荐指数
饿汉式
懒汉式(加锁) ✅ (性能差)
双重检查锁 ❌ (需防御)
静态内部类
枚举 ✅(实际是饿汉)
容器式
ThreadLocal 每线程唯一

案例选择建议

  • 单体应用无反射攻击风险:静态内部类(优雅且性能好)
  • 需要序列化/REST API:枚举(无后顾之忧)
  • 高并发且延迟加载:双重检查锁(必须加 volatile)
  • 框架内部管理多个单例:容器式

问:为什么枚举单例能防止反射? 答:JVM 禁止通过反射创建枚举实例(会抛 IllegalArgumentException),在反序列化时,枚举实例由 readObject 特殊处理,返回已有对象。


常见陷阱与面试高频问题

陷阱1:反射破坏单例

Constructor<?> constructor = SingletonHolder.class.getDeclaredConstructor();
constructor.setAccessible(true);
SingletonHolder instance2 = (SingletonHolder) constructor.newInstance();

防御:在构造函数中加标志位或使用枚举。

陷阱2:反序列化破坏

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
oos.writeObject(instance);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.ser"));
SingletonHolder instance2 = (SingletonHolder) ois.readObject();

修复:提供 readResolve() 方法返回实例。

陷阱3:多个类加载器

不同类加载器会创建不同实例,一般通过指定类加载器解决。

面试高频题

  1. 双重检查锁为什么要加 volatile?
  2. 懒汉式synchronized加在方法上和代码块上有什么区别?
  3. 单例是否适用于无状态服务?

性能测试与安全加固

性能测试代码片段(100万次并发获取):

// 双重检查锁平均耗时:15ms
// 静态内部类平均耗时:12ms
// 枚举平均耗时:8ms (初始化后无锁)

安全加固

  • 防止克隆:重写 clone() 返回异常
  • 防止反射:构造函数加计数器或使用枚举
  • 防止序列化:实现 readResolve()

Q&A 问答精选

Q1:为什么构造函数必须私有?
A:防止外部直接new,破坏唯一实例原则。

Q2:volatile 在单例中起什么作用?
A:防止创建对象时指令重排序导致未初始化的对象被访问。new SingletonDCL() 实际上三步:1.分配内存 2.初始化对象 3.引用指向内存,不加volatile可能发生2和3互换。

Q3:枚举单例真的能保证全局唯一吗?
A:能,JVM 保证枚举实例的跨类加载器唯一性,且天然反序列化安全。

Q4:什么时候不能用单例?
A:有状态服务如果要求每个请求状态独立,单例会导致数据污染,此时用原型模式。

Q5:Spring中的单例和设计模式单例一样吗?
A:类似但不完全一样,Spring默认Bean是单例的(属于对象池范畴),但通过IoC容器管理,不是通过私有构造函数实现。


单例模式看似简单,但深挖下去涉及JVM内存模型、类加载机制、并发控制等核心知识,实际开发中优先推荐静态内部类和枚举实现,尤其在有序列化需求的微服务环境下,枚举是最安全的选择,掌握这些变种和陷阱,不仅能在面试中游刃有余,更能写出更健壮的生产代码。

抱歉,评论功能暂时关闭!