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

相关推荐
jay神20 分钟前
【计算机毕业设计】基于Spring Boot的宠物领养管理系统
java·spring boot·后端·vue·毕业设计·宠物
cfm_291425 分钟前
5种Java并发同步工具全解
java
十五喵源码网29 分钟前
基于SpringBoot2+vue2的健身房管理系统
java·毕业设计·springboot·论文笔记
2601_9620566032 分钟前
可造成敏感信息泄露!Spring Boot之Actuator信息泄露漏洞三种利用方式总结
java·spring boot·后端
小小晓.1 小时前
C++ 值类别与完美转发深度剖析:从左值右值到 std::forward 源码解密
开发语言·c++·算法
garmin Chen1 小时前
redis面试题
java·数据库·redis·缓存
格林威2 小时前
C# 相机Burst模式图像采集:使用相机内存配合OpenCvSharp和Halcon实现短时间的高速采集的方法
开发语言·人工智能·数码相机·计算机视觉·c#·视觉检测·工业相机
chen<>2 小时前
C++ 六种 memory_order:从原子性到线程间可见性.md
java·linux·开发语言·c++
一直C2 小时前
Linux系统编程|进程间通信IPC全套详解(1)(管道、信号)
linux·c语言·开发语言·网络·青少年编程·visual studio code