前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

一、单例模式

单例模式是一种创建型设计模式,确保一个类在 同一个 JVM 中仅存在一个实例,并提供全局访问点。

1、作用和使用场景

1)作用

  1. 节省系统资源:避免频繁创建和销毁重量级对象(如数据库连接池、日志管理器等)。
  2. 保证全局唯一性:对核心控制类(如交易引擎、配置中心)必须全局唯一,防止逻辑混乱(例如多个“司令员”同时指挥)。

2)使用场景

场景 问题 单例如何解决
文件写入 多线程并发写导致混乱 提供唯一写入入口,内部同步
连接池初始化 重复创建耗尽资源 全局唯一实例,一次初始化
配置共享 模块间无法直接传参 全局可访问的配置容器

a、控制对共享资源的并发访问(如文件写入、缓存管理)

场景举例:日志记录器(Logger)

  • 多个线程同时需要写日志到同一个文件。
  • 如果每个线程都创建自己的 FileWriter,会导致:
    • 文件被多个流同时打开,可能损坏;
    • 日志内容交错混乱;
    • 资源浪费(大量文件句柄)。
public class Logger {
    private static final Logger INSTANCE = new Logger();
    private FileWriter writer;

    private Logger() {
        try {
            writer = new FileWriter("app.log", true); // 追加模式
        } catch (IOException e) { /* ... */ }
    }

    public static Logger getInstance() {
        return INSTANCE;
    }

    public synchronized void log(String message) {
        try {
            writer.write(LocalDateTime.now() + ": " + message + "\n");
            writer.flush();
        } catch (IOException e) { /* ... */ }
    }
}

b、节约内存和 CPU 开销,避免重复初始化

场景举例:数据库连接池(DataSource)

  • 初始化连接池(如 HikariCP、Druid)开销大:需建立多个物理连接、加载驱动、验证配置等。
  • 若每个 DAO 类都新建一个连接池,系统将迅速耗尽内存和数据库连接数。
public class DatabasePool {
    private static volatile DatabasePool instance;
    private HikariDataSource dataSource;

    private DatabasePool() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
        config.setUsername("root");
        config.setPassword("123456");
        config.setMaximumPoolSize(20);
        this.dataSource = new HikariDataSource(config);
    }

    public static DatabasePool getInstance() {
        if (instance == null) {
            synchronized (DatabasePool.class) {
                if (instance == null) {
                    instance = new DatabasePool();
                }
            }
        }
        return instance;
    }

    public Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }
}

c、在无直接依赖的情况下,实现跨模块/线程的数据共享

场景举例:应用配置中心(AppConfig)

  • 系统启动时从 application.properties 加载配置(如 API 密钥、超时时间、开关标志)。
  • 多个服务类(UserService、PaymentService、NotificationService)都需要读取这些配置,但彼此无调用关系。
public class AppConfig {
    private static class Holder {
        static final AppConfig INSTANCE = new AppConfig();
    }

    private Properties props = new Properties();

    private AppConfig() {
        try (InputStream in = getClass().getResourceAsStream("/application.properties")) {
            props.load(in);
        } catch (IOException e) {
            throw new RuntimeException("Failed to load config", e);
        }
    }

    public static AppConfig getInstance() {
        return Holder.INSTANCE;
    }

    public String getProperty(String key) {
        return props.getProperty(key);
    }

    // 示例:获取支付超时时间
    public int getPaymentTimeoutMs() {
        return Integer.parseInt(getProperty("payment.timeout.ms"));
    }
}

3)不是所有地方都“必须”用 final

场景 是否用 final 理由
单例实例(饿汉/静态内部类/枚举) 安全、清晰、可优化
单例类本身 防止继承破坏封装
配置类字段(如数据库 URL) 初始化后不应改变
方法参数(如工具方法) 推荐 防止意外修改,提升可读性
懒汉式 DCL 的 instance 字段 需要运行时赋值
会变化的状态字段 逻辑上需要修改

2、单例模式的 7 种写法

实现方式 线程安全 延迟加载 防反射 防反序列化 推荐度
懒汉(无锁)
懒汉(synchronized ⭐⭐
饿汉式 ⭐⭐
静态内部类 ⭐⭐⭐⭐
枚举 ✅* ⭐⭐⭐⭐⭐

1)懒汉式(线程不安全)

  • 问题:多线程环境下不安全。
  • 优点:延迟加载(Lazy Loading)。
public class Singleton {
    private static Singleton instance;
    
    private Singleton (){
        
    }

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton(); //// 多线程下可能创建多个实例
        }
        return instance;
    }
}

2)懒汉式(线程安全,方法加锁)

  • 缺点:每次调用都加锁,性能差(99% 的情况无需同步)。
    • synchronized 关键字锁住的是这个类对象,,在性能上会下降,因为每次调用 getInstance(),都要对对象上锁,事实上,只有在第一次创建对象的时候需要加锁,之后就不需要了,所以,这个地方需要改进
  • 优点:线程安全 + 延迟加载。
public class Singleton {
    private static Singleton instance;
    
    private Singleton (){
        
    }
    
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

3)饿汉式(静态变量)

  • 优点:线程安全(类加载时初始化),实现简单。
  • 缺点非延迟加载,即使未使用也会占用内存。
public class Singleton {
    private static final Singleton instance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}

4)饿汉式(静态代码块)

表面上看起来差别挺大,其实更第三种方式差不多,都是在类初始化即实例化 instance

public class Singleton {
    private static Singleton instance;

    static {
        instance = new Singleton();
    }

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}

5)静态内部类(推荐)

优点这是懒汉与饿汉的最佳结合

  • 利用类加载机制保证线程安全,保证初始化 instance 时只有一个线;
  • 真正实现延迟加载(只有调用 getInstance() 时才加载 Holder 类);
  • 无性能损耗。
public class Singleton {
    private Singleton() {}

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

    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }
}

6)枚举实现(最推荐)

优势

  • 天然线程安全JVM 保证枚举实例唯一);
  • 自动防止反射攻击Constructor.newInstance() 对枚举抛出异常);
  • 自动防止反序列化破坏(反序列化时通过 name 查找已有实例,而非新建);
  • 代码简洁,无额外开销。
public enum Singleton {
    INSTANCE;

    public void whateverMethod() {
    }
}

7)双重检查锁(需谨慎使用)

关键点

  • 两次判空:第一次避免不必要的同步,第二次防止多线程重复创建;
  • volatile 关键字:禁止指令重排序,确保其他线程看到的是完全初始化的对象。
public class SingletonFinal {  

    /* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */   
    private volatile static SingletonFinal instance = null;  

    /* 私有构造方法,防止被实例化 */  
    private SingletonFinal() {  
    }  

    /* 1、静态工程方法,创建实例 */  
    public static SingletonFinal getInstance() {  
        if (instance == null) {  
            syncInit();  
        }  
        return instance;  
    }
        
    private static synchronized void syncInit() {  
        if (instance == null) {  
            instance = new SingletonFinal();  
        }  
    }  
}  

3、单例模式的安全隐患

1)反射攻击

public class Singleton {  

    /* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */   
    private volatile  static Singleton instance = null;  

    /* 私有构造方法,防止被实例化 */  
    private SingletonFinal() {  
    }  

    /* 1、静态工程方法,创建实例 */  
    public static Singleton getInstance() {  
        if (instance == null) {  
            syncInit();  
        }  
        return instance;  
    }  
    
        
    private static synchronized void syncInit() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
    }  
}  

public static void main(String[] args) throws Exception {
    Singleton singleton = Singleton.getInstance();
    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    Singleton newSingleton = constructor.newInstance();
    System.out.println(singleton == newSingleton); //false
}

// 这两个实例不是同一个,这就违背了单例模式的原则了。

2)反序列化攻击

public class Singleton implements Serializable {

    private static class SingletonHolder {
        private static Singleton instance = new Singleton();
    }

    private Singleton() {

    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }

    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();
        byte[] serialize = SerializationUtils.serialize(instance);
        Singleton newInstance = SerializationUtils.deserialize(serialize);
        System.out.println(instance == newInstance); //false
        
        // 这两个实例不是同一个,这就违背了单例模式的原则了。
    }
}

4、单例模式缺点

1)不支持带参构造

无法在获取实例时传入参数(如连接池大小)。

应对策略

  • 参数通过配置文件、系统属性或静态 setter 注入;
  • 或考虑工厂模式 + 单例注册表

2)扩展性差

若未来需要多个实例(如多数据源),需重构代码。

建议:评估是否真的需要“全局唯一”,避免过度使用单例。

5、枚举类型实现的单例模式是最佳的方式

枚举方式实现的单例模式不仅能避免多线程同步的问题,也可以防止反序列化和反射的破坏。

public enum Singleton {

    INSTANCE;

    public void doSomething() {
        System.out.println("doSomething");
    }

}

1、JVM 级别的线程安全

反编译的代码中可以发现枚举中的各个枚举项都是通过 static 代码块来定义和初始化的,他们会在类被加载时完成初始化,而 Java 的类加载由 JVM 保证线程安全。

2、防止反序列化的破坏

  • 在序列化时,只是将枚举对象的 name属性输出到结果中,

  • 在反序列化时通过 java.lang.EnumvalueOf 方法根据名字查找对象,而不是新建一个新的对象,所以防止了反序列化对单例的破坏。

3、防止反射的破坏

反射在通过newInstance创建对象时会检查这个类是否是枚举类,如果是枚举类就会throw new IllegalArgumentException("Cannot reflectively create enum objects");,如下是源码java.lang.reflect.Constructor#newInstance