Java 单例模式:从基础到实战的完整指南

1. 什么是单例模式

单例模式(Singleton Pattern)是 Java 中最基础、最常用的创建型设计模式之一。它的核心思想是:保证一个类在整个应用程序生命周期中只有一个实例,并提供一个全局访问点

为什么需要单例?在实际开发中,有些对象只需要一个实例就够了,比如:

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

如果这些对象被反复创建,不仅浪费内存资源,还可能导致数据不一致、连接耗尽等问题。

2. 单例模式的实现方式

单例模式的实现方式有很多种,每种方式在线程安全、性能、延迟加载等方面各有取舍。下面逐一介绍。

2.1 饿汉式(Eager Initialization)

饿汉式在类加载时就创建实例,实现最简单,天然线程安全。

java 复制代码
public class EagerSingleton {
    // 类加载时即创建实例
    private static final EagerSingleton INSTANCE = new EagerSingleton();

    // 私有构造方法,防止外部 new
    private EagerSingleton() {}

    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}

优点 :实现简单,线程安全,无需加锁。

缺点:类加载时就创建实例,即使从未使用也会占用内存,造成资源浪费。

2.2 懒汉式(Lazy Initialization)

懒汉式在第一次调用 getInstance() 时才创建实例,实现了延迟加载。

java 复制代码
public class LazySingleton {
    private static LazySingleton instance;

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

优点 :延迟加载,节省资源。

缺点线程不安全 。多个线程同时调用 getInstance() 时,可能创建多个实例。

2.3 线程安全的懒汉式(同步方法)

getInstance() 方法上加 synchronized 关键字,保证线程安全。

java 复制代码
public class SynchronizedSingleton {
    private static SynchronizedSingleton instance;

    private SynchronizedSingleton() {}

    public static synchronized SynchronizedSingleton getInstance() {
        if (instance == null) {
            instance = new SynchronizedSingleton();
        }
        return instance;
    }
}

优点 :线程安全。

缺点 :每次调用 getInstance() 都要加锁,性能开销大。实际上只有第一次创建实例时才需要同步。

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

双重检查锁结合了懒加载和线程安全,是懒汉式的优化版本。

java 复制代码
public class DoubleCheckedSingleton {
    // volatile 防止指令重排序
    private static volatile DoubleCheckedSingleton instance;

    private DoubleCheckedSingleton() {}

    public static DoubleCheckedSingleton getInstance() {
        if (instance == null) {                    // 第一次检查(不加锁)
            synchronized (DoubleCheckedSingleton.class) {
                if (instance == null) {            // 第二次检查(加锁)
                    instance = new DoubleCheckedSingleton();
                }
            }
        }
        return instance;
    }
}

关键点instance 必须用 volatile 修饰。因为 new DoubleCheckedSingleton() 不是原子操作,分为三步:

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

如果没有 volatile,JVM 可能对第 2、3 步进行指令重排序,导致其他线程拿到一个未初始化完成的对象。

优点 :延迟加载 + 线程安全 + 性能较好(只有第一次创建时才加锁)。

缺点 :实现稍复杂,需要理解 volatile 的原理。

2.5 静态内部类(推荐)

利用 JVM 的类加载机制保证线程安全,同时实现延迟加载,是最推荐的实现方式

java 复制代码
public class StaticInnerSingleton {

    private StaticInnerSingleton() {}

    // 静态内部类,只有被调用时才加载
    private static class Holder {
        private static final StaticInnerSingleton INSTANCE = new StaticInnerSingleton();
    }

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

原理 :JVM 在加载 StaticInnerSingleton 时不会加载 Holder 内部类,只有第一次调用 getInstance() 时才会加载 Holder 并创建实例。JVM 保证类加载过程是线程安全的,因此无需额外加锁。

优点 :延迟加载 + 线程安全 + 实现简洁。

缺点:无法在创建实例时传入参数。

2.6 枚举方式(最简洁)

枚举是《Effective Java》作者 Joshua Bloch 推荐的实现方式,也是最简洁、最安全的单例实现

java 复制代码
public enum EnumSingleton {
    INSTANCE;

    // 可以添加业务方法
    public void doSomething() {
        System.out.println("枚举单例方法调用");
    }
}

使用方式:

java 复制代码
EnumSingleton.INSTANCE.doSomething();

优点

  • 实现最简单
  • 天然线程安全
  • 天然防止反射攻击和序列化破坏

缺点:无法实现延迟加载(枚举常量在类加载时创建)。

3. 单例模式的破坏与防护

单例模式虽然保证了构造方法私有,但仍可能被以下方式破坏:

3.1 反射破坏

通过反射可以绕过私有构造方法的限制:

java 复制代码
Class<?> clazz = Singleton.class;
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton instance1 = (Singleton) constructor.newInstance();
Singleton instance2 = Singleton.getInstance();
// instance1 != instance2,单例被破坏

防护方案:在构造方法中增加判断,如果实例已存在则抛出异常。

java 复制代码
public class SafeSingleton {
    private static volatile SafeSingleton instance;

    private SafeSingleton() {
        if (instance != null) {
            throw new RuntimeException("禁止通过反射创建实例");
        }
    }
}

3.2 序列化破坏

如果单例类实现了 Serializable 接口,反序列化时会创建新实例。

防护方案 :重写 readResolve() 方法。

java 复制代码
public class SerializableSingleton implements Serializable {
    private static final long serialVersionUID = 1L;
    private static final SerializableSingleton INSTANCE = new SerializableSingleton();

    private SerializableSingleton() {}

    public static SerializableSingleton getInstance() {
        return INSTANCE;
    }

    // 反序列化时返回已有实例
    protected Object readResolve() {
        return INSTANCE;
    }
}

4. 单例模式的应用场景

单例模式在真实项目中应用广泛,以下是几个典型场景:

4.1 数据库连接池

java 复制代码
public class DatabaseConnectionPool {
    private static volatile DatabaseConnectionPool pool;
    private List<Connection> connections;

    private DatabaseConnectionPool() {
        // 初始化连接池
        connections = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            connections.add(createConnection());
        }
    }

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

    public Connection getConnection() {
        // 从池中获取连接
        return connections.remove(0);
    }
}

4.2 配置管理器

java 复制代码
public class ConfigManager {
    private static final ConfigManager INSTANCE = new ConfigManager();
    private Properties properties;

    private ConfigManager() {
        properties = new Properties();
        // 加载配置文件
        try (InputStream in = getClass().getClassLoader()
                .getResourceAsStream("config.properties")) {
            properties.load(in);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static ConfigManager getInstance() {
        return INSTANCE;
    }

    public String get(String key) {
        return properties.getProperty(key);
    }
}

5. 单例模式总结

实现方式 延迟加载 线程安全 性能 推荐度
饿汉式 ⭐⭐⭐
懒汉式
同步方法 ⭐⭐
双重检查锁 ⭐⭐⭐⭐
静态内部类 ⭐⭐⭐⭐⭐
枚举 ⭐⭐⭐⭐⭐

选择建议

  • 如果实例一定会被使用,且创建开销不大,用饿汉式即可。
  • 如果需要延迟加载,优先选择静态内部类,实现简洁且线程安全。
  • 如果需要防止反射和序列化破坏,使用枚举
  • 面试中常考双重检查锁 ,需要理解 volatile 的作用。

6. 常见面试问题

Q1:单例模式为什么要有私有构造方法?

防止外部通过 new 关键字创建实例,保证全局只有一个实例。

Q2:双重检查锁中 volatile 的作用是什么?

防止指令重排序。new 操作不是原子操作,volatile 保证其他线程不会读取到未初始化完成的对象。

Q3:静态内部类为什么是线程安全的?

JVM 保证类的加载过程是线程安全的,多个线程同时加载同一个类时,只有一个线程会执行类初始化,其他线程会等待。

Q4:枚举单例为什么能防止反射破坏?

枚举类的构造方法在 JVM 层面有特殊保护,反射调用 newInstance() 时会抛出 IllegalArgumentException

相关推荐
Joy T3 小时前
Spring AI 2.0 Agent 进阶:Memory、State 与 Context Engineering 常见技术全景
java·人工智能·后端·spring·agent入门·agent state
估值探索者3 小时前
【Python量化系统工程化 #08】关了 SSH 就停?systemd 让脚本开机自启 + 异常自动拉起
java·c++·人工智能·分类·数据挖掘
RAN11426760083 小时前
1.3第一个程序的介绍
开发语言·qt
天若有情6733 小时前
C++花式魔改!手写头文件复刻Java语法,彻底打破编码思维定式
java·c++·语言做语言·jac++
blue_ice .4 小时前
Verilog DDS 数字信号发生器:DAC 输出模拟波形与 ADC 回采观测
开发语言·单片机·嵌入式硬件·fpga开发
远翔调光芯片^138287988724 小时前
ECP5702能芯科技PD取电芯片在市场上的优势有哪些?
开发语言·人工智能·单片机·嵌入式硬件·智能家居
西峰u5 小时前
Java单例模式|从基础到多线程安全
java·java单例模式
ClinicTech5 小时前
实验室洗瓶机的清洗系统架构与自动化控制解析
java·python
linmengmeng_13145 小时前
【总结】MyBatis-Plus批量插入与清表的正确姿势
java·spring boot·mysql·mybatis
木井巳5 小时前
【记忆化搜索】不同路径
java·算法·leetcode·深度优先·剪枝·推荐算法