单例模式深度解析:如何确保一个类只有一个实例

单例模式是设计模式中最简单也最常用的模式之一,它确保一个类只有一个实例,并提供一个全局访问点。这在需要控制资源访问或共享状态的场景中非常有用。

文章目录

    • [🌟 引言:一个真实世界的问题](#🌟 引言:一个真实世界的问题)
    • 一、什么是单例模式?🎯
      • [1.1 官方定义](#1.1 官方定义)
      • [1.2 核心特点](#1.2 核心特点)
      • [1.3 形象的比喻](#1.3 形象的比喻)
    • [二、单例模式的实现方式 🔧](#二、单例模式的实现方式 🔧)
      • [2.1 饿汉式(Eager Initialization)](#2.1 饿汉式(Eager Initialization))
      • [2.2 懒汉式(Lazy Initialization)](#2.2 懒汉式(Lazy Initialization))
      • [2.3 线程安全的懒汉式](#2.3 线程安全的懒汉式)
      • [2.4 枚举单例(最佳实践)](#2.4 枚举单例(最佳实践))
      • [2.5 各种实现方式对比](#2.5 各种实现方式对比)
    • [三、单例模式的使用场景 🎯](#三、单例模式的使用场景 🎯)
      • [3.1 场景一:配置管理器](#3.1 场景一:配置管理器)
      • [3.2 场景二:数据库连接池](#3.2 场景二:数据库连接池)
      • [3.3 场景三:日志记录器](#3.3 场景三:日志记录器)
      • [3.4 场景四:缓存管理器](#3.4 场景四:缓存管理器)
      • [3.5 更多使用场景总结](#3.5 更多使用场景总结)
    • [四、单例模式的优缺点 ⚖️](#四、单例模式的优缺点 ⚖️)
      • [4.1 优点](#4.1 优点)
      • [4.2 缺点和注意事项](#4.2 缺点和注意事项)
      • [4.3 单例模式的替代方案](#4.3 单例模式的替代方案)
    • [五、面试常见问题与回答 💼](#五、面试常见问题与回答 💼)
      • [5.1 基础问题](#5.1 基础问题)
      • [5.2 进阶问题](#5.2 进阶问题)
      • [5.3 实战问题](#5.3 实战问题)
    • [六、总结与最佳实践 📚](#六、总结与最佳实践 📚)
      • [6.1 核心要点回顾](#6.1 核心要点回顾)
      • [6.2 选择指南](#6.2 选择指南)
      • [6.3 最佳实践建议](#6.3 最佳实践建议)
      • [6.4 实际项目建议](#6.4 实际项目建议)
    • [参考资料 📖](#参考资料 📖)

🌟 引言:一个真实世界的问题

想象一下这个场景:在一个大型电商系统中,配置管理器负责读取和管理所有系统配置。如果每个模块都自己创建配置管理器实例,会出现什么问题?

  1. 内存浪费:每个实例都加载一次配置文件,占用额外内存
  2. 配置不一致:不同实例可能有不同的配置状态
  3. 资源冲突:多个实例同时写入配置可能导致数据损坏

这就是单例模式要解决的核心问题确保一个类只有一个实例,并提供全局访问点

java 复制代码
// 错误示例:多个配置管理器实例
public class ConfigManagerBadExample {
    private Properties config;
    
    public ConfigManagerBadExample() {
        // 每次创建都重新加载配置
        loadConfig();
    }
    
    private void loadConfig() {
        // 耗时操作:读取文件、解析配置
        System.out.println("加载配置文件...");
    }
    
    public String getConfig(String key) {
        return config.getProperty(key);
    }
}

// 使用
ConfigManagerBadExample manager1 = new ConfigManagerBadExample();
ConfigManagerBadExample manager2 = new ConfigManagerBadExample();
// 问题:创建了两个实例,配置加载了两次!

一、什么是单例模式?🎯

1.1 官方定义

单例模式(Singleton Pattern) 是一种创建型设计模式,它保证一个类只有一个实例 ,并提供一个全局访问点来访问这个实例。

1.2 核心特点

特点 说明 重要性
唯一实例 类只能有一个实例 ⭐⭐⭐⭐⭐
全局访问 提供全局访问点 ⭐⭐⭐⭐
自行创建 自己创建自己的实例 ⭐⭐⭐
控制构造 构造函数私有化 ⭐⭐⭐⭐⭐

1.3 形象的比喻

现实世界类比 对应关系 解释
公司CEO 单例实例 一个公司只能有一个CEO
太阳系太阳 单例实例 太阳系只有一个太阳
国家总统 单例实例 一个国家在任总统只有一个
班级班长 单例实例 一个班级只有一个班长

二、单例模式的实现方式 🔧

单例模式有多种实现方式,各有优缺点。让我们从最简单到最完善的顺序来学习:

2.1 饿汉式(Eager Initialization)

特点:类加载时就创建实例,线程安全但可能浪费资源

java 复制代码
/**
 * 饿汉式单例
 * 优点:实现简单,线程安全
 * 缺点:类加载时就初始化,可能浪费内存
 */
public class EagerSingleton {
    
    // 1. 私有静态实例,类加载时创建
    private static final EagerSingleton INSTANCE = new EagerSingleton();
    
    // 2. 私有构造函数,防止外部创建实例
    private EagerSingleton() {
        System.out.println("EagerSingleton 实例被创建");
        // 初始化操作
    }
    
    // 3. 全局访问点
    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
    
    // 业务方法
    public void showMessage() {
        System.out.println("Hello from EagerSingleton!");
    }
    
    public static void main(String[] args) {
        // 测试
        EagerSingleton singleton1 = EagerSingleton.getInstance();
        EagerSingleton singleton2 = EagerSingleton.getInstance();
        
        System.out.println("singleton1 == singleton2: " + (singleton1 == singleton2));
        // 输出:true,证明是同一个实例
    }
}

适用场景

  • 实例占用资源少
  • 初始化不耗时
  • 确定会用到这个实例

2.2 懒汉式(Lazy Initialization)

特点:第一次使用时才创建实例,但线程不安全

java 复制代码
/**
 * 懒汉式单例(基础版)
 * 优点:延迟加载,节省资源
 * 缺点:线程不安全,多线程环境下可能创建多个实例
 */
public class LazySingleton {
    
    // 1. 私有静态实例,初始为null
    private static LazySingleton instance;
    
    // 2. 私有构造函数
    private LazySingleton() {
        System.out.println("LazySingleton 实例被创建");
    }
    
    // 3. 全局访问点(线程不安全!)
    public static LazySingleton getInstance() {
        if (instance == null) {
            // 多个线程可能同时进入这里
            instance = new LazySingleton();
        }
        return instance;
    }
    
    // 业务方法
    public void showMessage() {
        System.out.println("Hello from LazySingleton!");
    }
}

线程安全问题演示

java 复制代码
public class SingletonThreadTest {
    public static void main(String[] args) {
        // 模拟多线程环境
        Runnable task = () -> {
            LazySingleton singleton = LazySingleton.getInstance();
            System.out.println(Thread.currentThread().getName() + 
                             " 获取实例: " + singleton.hashCode());
        };
        
        // 创建多个线程
        Thread thread1 = new Thread(task, "线程1");
        Thread thread2 = new Thread(task, "线程2");
        Thread thread3 = new Thread(task, "线程3");
        
        thread1.start();
        thread2.start();
        thread3.start();
        
        // 可能输出不同的hashCode,证明创建了多个实例
    }
}

2.3 线程安全的懒汉式

方式一:同步方法(效率低)
java 复制代码
/**
 * 线程安全的懒汉式(同步方法)
 * 优点:线程安全,实现简单
 * 缺点:每次获取实例都要同步,性能差
 */
public class ThreadSafeLazySingleton {
    
    private static ThreadSafeLazySingleton instance;
    
    private ThreadSafeLazySingleton() {
        System.out.println("ThreadSafeLazySingleton 实例被创建");
    }
    
    // 使用synchronized保证线程安全
    public static synchronized ThreadSafeLazySingleton getInstance() {
        if (instance == null) {
            instance = new ThreadSafeLazySingleton();
        }
        return instance;
    }
}
方式二:双重检查锁定(Double-Checked Locking)
java 复制代码
/**
 * 双重检查锁定单例
 * 优点:线程安全,延迟加载,性能好
 * 注意:需要volatile防止指令重排序
 */
public class DoubleCheckedSingleton {
    
    // 使用volatile确保可见性和防止指令重排序
    private static volatile DoubleCheckedSingleton instance;
    
    private DoubleCheckedSingleton() {
        System.out.println("DoubleCheckedSingleton 实例被创建");
    }
    
    public static DoubleCheckedSingleton getInstance() {
        // 第一次检查:避免不必要的同步
        if (instance == null) {
            synchronized (DoubleCheckedSingleton.class) {
                // 第二次检查:确保只有一个线程创建实例
                if (instance == null) {
                    instance = new DoubleCheckedSingleton();
                    // 创建对象分三步:
                    // 1. 分配内存空间
                    // 2. 初始化对象
                    // 3. 设置instance指向分配的内存
                    // volatile防止2和3步骤重排序
                }
            }
        }
        return instance;
    }
}

volatile的重要性

java 复制代码
// 没有volatile时可能发生的问题:
// 线程A执行:instance = new DoubleCheckedSingleton();
// 可能发生的指令重排序:
// 1. 分配内存空间
// 3. 设置instance指向内存空间(此时instance不为null,但对象未初始化)
// 2. 初始化对象
// 线程B执行:if (instance == null) 判断为false,返回未完全初始化的对象!
方式三:静态内部类(推荐)
java 复制代码
/**
 * 静态内部类单例(推荐使用)
 * 优点:线程安全,延迟加载,实现简单,性能好
 * 原理:利用类加载机制保证线程安全
 */
public class InnerClassSingleton {
    
    // 私有构造函数
    private InnerClassSingleton() {
        System.out.println("InnerClassSingleton 实例被创建");
    }
    
    // 静态内部类
    private static class SingletonHolder {
        private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
    }
    
    // 全局访问点
    public static InnerClassSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
    
    // 业务方法
    public void showMessage() {
        System.out.println("Hello from InnerClassSingleton!");
    }
    
    // 工作原理说明:
    // 1. 当调用getInstance()时,才会加载SingletonHolder类
    // 2. 类加载是线程安全的,由JVM保证
    // 3. 实现了延迟加载和线程安全
}

2.4 枚举单例(最佳实践)

java 复制代码
/**
 * 枚举单例(Effective Java推荐)
 * 优点:线程安全,防止反射攻击,防止序列化破坏单例
 * 缺点:不够灵活(不能延迟加载)
 */
public enum EnumSingleton {
    
    INSTANCE;  // 唯一的实例
    
    // 可以添加实例变量和方法
    private String config;
    
    // 枚举的构造函数默认是private的
    EnumSingleton() {
        System.out.println("EnumSingleton 实例被创建");
        config = loadConfig();
    }
    
    private String loadConfig() {
        // 加载配置
        return "默认配置";
    }
    
    // 业务方法
    public void doSomething() {
        System.out.println("使用配置: " + config);
    }
    
    public String getConfig() {
        return config;
    }
    
    public void setConfig(String config) {
        this.config = config;
    }
}

// 使用示例
public class EnumSingletonDemo {
    public static void main(String[] args) {
        // 获取单例实例
        EnumSingleton instance1 = EnumSingleton.INSTANCE;
        EnumSingleton instance2 = EnumSingleton.INSTANCE;
        
        System.out.println("instance1 == instance2: " + (instance1 == instance2));
        // 输出:true
        
        instance1.doSomething();
        instance2.setConfig("新配置");
        System.out.println(instance1.getConfig()); // 输出:新配置
    }
}

2.5 各种实现方式对比

实现方式 线程安全 延迟加载 性能 防止反射攻击 防止序列化破坏 推荐度
饿汉式 ⭐⭐⭐⭐ ⭐⭐
懒汉式(基础) ⭐⭐⭐⭐
同步方法懒汉式 ⭐⭐
双重检查锁定 ⭐⭐⭐ ⭐⭐⭐
静态内部类 ⭐⭐⭐⭐ ⭐⭐⭐⭐
枚举 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐

三、单例模式的使用场景 🎯

3.1 场景一:配置管理器

问题:系统配置需要全局唯一且一致

java 复制代码
/**
 * 配置管理器单例
 * 读取配置文件,提供全局配置访问
 */
public class ConfigManager {
    
    // 静态内部类实现单例
    private static class Holder {
        private static final ConfigManager INSTANCE = new ConfigManager();
    }
    
    private Properties properties;
    
    private ConfigManager() {
        loadConfig();
    }
    
    public static ConfigManager getInstance() {
        return Holder.INSTANCE;
    }
    
    // 加载配置文件
    private void loadConfig() {
        properties = new Properties();
        try {
            // 从classpath加载配置文件
            InputStream input = getClass()
                .getClassLoader()
                .getResourceAsStream("application.properties");
            
            if (input != null) {
                properties.load(input);
                System.out.println("配置文件加载成功");
            } else {
                System.err.println("配置文件未找到,使用默认配置");
                setDefaultConfig();
            }
        } catch (IOException e) {
            System.err.println("加载配置文件失败: " + e.getMessage());
            setDefaultConfig();
        }
    }
    
    private void setDefaultConfig() {
        properties.setProperty("database.url", "jdbc:mysql://localhost:3306/mydb");
        properties.setProperty("database.username", "root");
        properties.setProperty("database.password", "password");
        properties.setProperty("server.port", "8080");
        properties.setProperty("log.level", "INFO");
    }
    
    // 获取配置值
    public String getProperty(String key) {
        return properties.getProperty(key);
    }
    
    public String getProperty(String key, String defaultValue) {
        return properties.getProperty(key, defaultValue);
    }
    
    // 更新配置(同步保护)
    public synchronized void setProperty(String key, String value) {
        properties.setProperty(key, value);
        // 可以添加配置持久化逻辑
    }
    
    // 获取所有配置
    public Properties getAllProperties() {
        return new Properties(properties); // 返回副本,保护内部数据
    }
}

// 使用示例
public class ConfigManagerDemo {
    public static void main(String[] args) {
        // 在整个应用程序的任何地方都可以这样获取配置
        ConfigManager config = ConfigManager.getInstance();
        
        // 读取配置
        String dbUrl = config.getProperty("database.url");
        String serverPort = config.getProperty("server.port", "8080");
        
        System.out.println("数据库URL: " + dbUrl);
        System.out.println("服务器端口: " + serverPort);
        
        // 在Web应用的Servlet中
        // 在Service层中
        // 在任何需要配置的地方...
        // 都是同一个ConfigManager实例
    }
}

3.2 场景二:数据库连接池

问题:数据库连接是昂贵资源,需要重用和管理

java 复制代码
/**
 * 数据库连接池单例
 * 管理数据库连接,避免频繁创建和销毁连接
 */
public class DatabaseConnectionPool {
    
    // 双重检查锁定实现
    private static volatile DatabaseConnectionPool instance;
    
    // 连接池
    private List<Connection> connectionPool;
    private List<Connection> usedConnections;
    private final int INITIAL_POOL_SIZE = 10;
    private final int MAX_POOL_SIZE = 20;
    
    private final String url;
    private final String user;
    private final String password;
    
    private DatabaseConnectionPool() {
        // 从配置读取数据库连接信息
        ConfigManager config = ConfigManager.getInstance();
        this.url = config.getProperty("database.url");
        this.user = config.getProperty("database.username");
        this.password = config.getProperty("database.password");
        
        // 初始化连接池
        connectionPool = new ArrayList<>(INITIAL_POOL_SIZE);
        usedConnections = new ArrayList<>();
        
        initializePool();
    }
    
    public static DatabaseConnectionPool getInstance() {
        if (instance == null) {
            synchronized (DatabaseConnectionPool.class) {
                if (instance == null) {
                    instance = new DatabaseConnectionPool();
                }
            }
        }
        return instance;
    }
    
    private void initializePool() {
        for (int i = 0; i < INITIAL_POOL_SIZE; i++) {
            connectionPool.add(createConnection());
        }
        System.out.println("数据库连接池初始化完成,初始大小: " + INITIAL_POOL_SIZE);
    }
    
    private Connection createConnection() {
        try {
            // 实际项目中会使用DataSource
            return DriverManager.getConnection(url, user, password);
        } catch (SQLException e) {
            throw new RuntimeException("创建数据库连接失败", e);
        }
    }
    
    // 获取连接
    public synchronized Connection getConnection() {
        if (connectionPool.isEmpty()) {
            if (usedConnections.size() < MAX_POOL_SIZE) {
                // 创建新连接
                connectionPool.add(createConnection());
            } else {
                throw new RuntimeException("连接池已满,无法获取连接");
            }
        }
        
        Connection connection = connectionPool.remove(connectionPool.size() - 1);
        usedConnections.add(connection);
        
        return connection;
    }
    
    // 释放连接(实际是放回连接池)
    public synchronized boolean releaseConnection(Connection connection) {
        usedConnections.remove(connection);
        return connectionPool.add(connection);
    }
    
    // 关闭所有连接
    public synchronized void shutdown() {
        for (Connection connection : connectionPool) {
            try {
                connection.close();
            } catch (SQLException e) {
                // 记录日志
            }
        }
        
        for (Connection connection : usedConnections) {
            try {
                connection.close();
            } catch (SQLException e) {
                // 记录日志
            }
        }
        
        connectionPool.clear();
        usedConnections.clear();
        
        System.out.println("数据库连接池已关闭");
    }
    
    // 获取连接池状态
    public void printPoolStatus() {
        System.out.println("连接池状态: 空闲=" + connectionPool.size() + 
                         ", 使用中=" + usedConnections.size() +
                         ", 总量=" + (connectionPool.size() + usedConnections.size()));
    }
}

// 使用示例
public class DatabasePoolDemo {
    public static void main(String[] args) {
        // 获取连接池单例
        DatabaseConnectionPool pool = DatabaseConnectionPool.getInstance();
        
        // 在多个地方使用同一个连接池
        Thread userServiceThread = new Thread(() -> {
            Connection conn = pool.getConnection();
            // 执行用户相关数据库操作
            try {
                // 使用连接...
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                pool.releaseConnection(conn);
            }
        }, "用户服务线程");
        
        Thread orderServiceThread = new Thread(() -> {
            Connection conn = pool.getConnection();
            // 执行订单相关数据库操作
            try {
                // 使用连接...
                Thread.sleep(1500);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                pool.releaseConnection(conn);
            }
        }, "订单服务线程");
        
        userServiceThread.start();
        orderServiceThread.start();
        
        // 等待线程完成
        try {
            userServiceThread.join();
            orderServiceThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        pool.printPoolStatus();
        pool.shutdown();
    }
}

3.3 场景三:日志记录器

问题:日志需要统一格式和输出位置

java 复制代码
/**
 * 日志记录器单例
 * 统一管理日志记录,支持不同级别和输出目标
 */
public class Logger {
    
    // 枚举单例实现
    public enum LogLevel {
        DEBUG, INFO, WARN, ERROR
    }
    
    // 单例实例
    private static volatile Logger instance;
    
    private LogLevel currentLevel = LogLevel.INFO;
    private PrintWriter fileWriter;
    private boolean consoleOutput = true;
    
    private Logger() {
        // 初始化日志文件
        try {
            String logFile = ConfigManager.getInstance()
                .getProperty("log.file", "application.log");
            fileWriter = new PrintWriter(new FileWriter(logFile, true), true);
            System.out.println("日志记录器初始化完成,日志文件: " + logFile);
        } catch (IOException e) {
            System.err.println("无法初始化日志文件: " + e.getMessage());
            fileWriter = null;
        }
    }
    
    public static Logger getInstance() {
        if (instance == null) {
            synchronized (Logger.class) {
                if (instance == null) {
                    instance = new Logger();
                }
            }
        }
        return instance;
    }
    
    // 设置日志级别
    public void setLevel(LogLevel level) {
        this.currentLevel = level;
    }
    
    // 记录日志
    public void log(LogLevel level, String message) {
        if (level.ordinal() < currentLevel.ordinal()) {
            return; // 低于当前级别的日志不记录
        }
        
        String logMessage = String.format("[%s] [%s] %s: %s",
            Thread.currentThread().getName(),
            getCurrentTime(),
            level,
            message);
        
        // 控制台输出
        if (consoleOutput) {
            System.out.println(logMessage);
        }
        
        // 文件输出
        if (fileWriter != null) {
            fileWriter.println(logMessage);
        }
    }
    
    // 便捷方法
    public void debug(String message) {
        log(LogLevel.DEBUG, message);
    }
    
    public void info(String message) {
        log(LogLevel.INFO, message);
    }
    
    public void warn(String message) {
        log(LogLevel.WARN, message);
    }
    
    public void error(String message) {
        log(LogLevel.ERROR, message);
    }
    
    public void error(String message, Throwable throwable) {
        log(LogLevel.ERROR, message + " - " + throwable.getMessage());
        if (fileWriter != null) {
            throwable.printStackTrace(fileWriter);
        }
    }
    
    private String getCurrentTime() {
        return LocalDateTime.now().format(
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
    }
    
    // 关闭资源
    public void shutdown() {
        if (fileWriter != null) {
            fileWriter.close();
        }
    }
}

// 使用示例
public class LoggerDemo {
    public static void main(String[] args) {
        // 获取日志记录器单例
        Logger logger = Logger.getInstance();
        
        // 在不同类中使用同一个日志记录器
        logger.info("应用程序启动");
        logger.debug("调试信息");
        
        // 模拟业务逻辑中的日志记录
        new UserService().processUser();
        new OrderService().processOrder();
        
        // 错误日志
        try {
            throw new RuntimeException("测试异常");
        } catch (RuntimeException e) {
            logger.error("发生异常", e);
        }
        
        logger.info("应用程序关闭");
        logger.shutdown();
    }
}

// 模拟业务类
class UserService {
    private static final Logger logger = Logger.getInstance();
    
    public void processUser() {
        logger.info("开始处理用户");
        // 业务逻辑...
        logger.debug("用户处理完成");
    }
}

class OrderService {
    private static final Logger logger = Logger.getInstance();
    
    public void processOrder() {
        logger.info("开始处理订单");
        // 业务逻辑...
        logger.warn("订单金额超过阈值");
    }
}

3.4 场景四:缓存管理器

问题:缓存需要全局共享和统一管理

java 复制代码
/**
 * 缓存管理器单例
 * 提供全局缓存服务,支持过期时间和最大大小限制
 */
public class CacheManager {
    
    // 静态内部类实现
    private static class Holder {
        private static final CacheManager INSTANCE = new CacheManager();
    }
    
    // 缓存项内部类
    private static class CacheItem {
        Object value;
        long expiryTime; // 过期时间戳
        
        CacheItem(Object value, long ttl) {
            this.value = value;
            this.expiryTime = System.currentTimeMillis() + ttl;
        }
        
        boolean isExpired() {
            return System.currentTimeMillis() > expiryTime;
        }
    }
    
    private final Map<String, CacheItem> cache;
    private final int MAX_CACHE_SIZE = 1000;
    private final long DEFAULT_TTL = 60 * 1000; // 默认60秒
    
    private CacheManager() {
        // 使用LinkedHashMap实现LRU缓存
        cache = new LinkedHashMap<String, CacheItem>(16, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry eldest) {
                return size() > MAX_CACHE_SIZE;
            }
        };
        
        // 启动清理线程
        startCleanupThread();
    }
    
    public static CacheManager getInstance() {
        return Holder.INSTANCE;
    }
    
    // 放入缓存
    public synchronized void put(String key, Object value) {
        put(key, value, DEFAULT_TTL);
    }
    
    public synchronized void put(String key, Object value, long ttl) {
        cache.put(key, new CacheItem(value, ttl));
    }
    
    // 获取缓存
    public synchronized Object get(String key) {
        CacheItem item = cache.get(key);
        if (item == null) {
            return null;
        }
        
        if (item.isExpired()) {
            cache.remove(key);
            return null;
        }
        
        return item.value;
    }
    
    // 移除缓存
    public synchronized void remove(String key) {
        cache.remove(key);
    }
    
    // 清空缓存
    public synchronized void clear() {
        cache.clear();
    }
    
    // 获取缓存大小
    public synchronized int size() {
        return cache.size();
    }
    
    // 启动清理线程(定期清理过期缓存)
    private void startCleanupThread() {
        Thread cleanupThread = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    Thread.sleep(30 * 1000); // 每30秒清理一次
                    cleanupExpiredItems();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }, "Cache-Cleanup-Thread");
        
        cleanupThread.setDaemon(true);
        cleanupThread.start();
    }
    
    // 清理过期项
    private synchronized void cleanupExpiredItems() {
        Iterator<Map.Entry<String, CacheItem>> iterator = cache.entrySet().iterator();
        int removedCount = 0;
        
        while (iterator.hasNext()) {
            Map.Entry<String, CacheItem> entry = iterator.next();
            if (entry.getValue().isExpired()) {
                iterator.remove();
                removedCount++;
            }
        }
        
        if (removedCount > 0) {
            System.out.println("清理了 " + removedCount + " 个过期缓存项");
        }
    }
}

// 使用示例
public class CacheManagerDemo {
    public static void main(String[] args) {
        // 获取缓存管理器单例
        CacheManager cache = CacheManager.getInstance();
        
        // 模拟商品服务
        ProductService productService = new ProductService();
        
        // 第一次请求:从数据库获取并缓存
        Product product1 = productService.getProductById(1001);
        System.out.println("商品1: " + product1.getName());
        
        // 第二次请求:从缓存获取(快速)
        Product product2 = productService.getProductById(1001);
        System.out.println("商品2: " + product2.getName() + " (来自缓存)");
        
        // 缓存统计
        System.out.println("缓存大小: " + cache.size());
        
        // 等待缓存过期
        try {
            System.out.println("等待缓存过期...");
            Thread.sleep(70 * 1000); // 等待70秒,超过默认TTL
            
            // 第三次请求:缓存已过期,重新从数据库获取
            Product product3 = productService.getProductById(1001);
            System.out.println("商品3: " + product3.getName() + " (重新加载)");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

// 模拟商品类
class Product {
    private int id;
    private String name;
    private double price;
    
    // 构造方法、getter、setter...
}

// 模拟商品服务
class ProductService {
    private static final CacheManager cache = CacheManager.getInstance();
    
    // 模拟数据库查询
    private Product queryFromDatabase(int productId) {
        System.out.println("从数据库查询商品: " + productId);
        // 模拟耗时操作
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // 模拟返回数据
        Product product = new Product();
        product.setId(productId);
        product.setName("商品" + productId);
        product.setPrice(99.99);
        return product;
    }
    
    public Product getProductById(int productId) {
        String cacheKey = "product:" + productId;
        
        // 先尝试从缓存获取
        Product product = (Product) cache.get(cacheKey);
        
        if (product == null) {
            // 缓存未命中,从数据库查询
            product = queryFromDatabase(productId);
            
            // 放入缓存,TTL设置为30秒
            cache.put(cacheKey, product, 30 * 1000);
            
            System.out.println("商品放入缓存: " + productId);
        } else {
            System.out.println("从缓存获取商品: " + productId);
        }
        
        return product;
    }
}

3.5 更多使用场景总结

场景 为什么需要单例 示例
线程池 避免重复创建线程,统一管理线程资源 Executors.newFixedThreadPool()
Spring容器 整个应用只需要一个IoC容器 ApplicationContext
任务调度器 统一调度和管理所有定时任务 ScheduledExecutorService
全局计数器 需要全局共享的计数状态 网站访问计数器
设备管理器 管理唯一硬件设备(如打印机) PrintService
全局锁管理器 管理分布式锁 Redis分布式锁管理器
消息队列连接 维护唯一的消息队列连接 RabbitMQ连接管理器
应用程序上下文 存储全局应用状态 ServletContext

四、单例模式的优缺点 ⚖️

4.1 优点

优点 说明 示例
严格控制实例数量 确保只有一个实例 配置管理器避免配置不一致
全局访问点 方便访问 Logger.getInstance()在任何地方都可调用
节约系统资源 避免重复创建 数据库连接池重用连接
延迟初始化 需要时才创建 大型对象使用时才加载
避免状态不一致 共享状态统一 缓存数据一致性

4.2 缺点和注意事项

缺点 说明 解决方案
违反单一职责原则 同时负责创建和管理自己的生命周期 使用依赖注入框架(如Spring)
难以单元测试 全局状态影响测试隔离性 使用接口,支持测试时替换实现
隐藏依赖关系 依赖关系不明显 使用依赖注入,明确依赖
可能成为性能瓶颈 所有访问都通过单一点 合理设计,避免在单例中放耗时操作
不支持有参构造 传统实现只能无参构造 使用初始化方法或Builder模式

4.3 单例模式的替代方案

在某些情况下,可以考虑以下替代方案:

java 复制代码
// 替代方案1:依赖注入(如Spring的@Bean)
@Configuration
public class AppConfig {
    
    @Bean
    @Scope("singleton") // Spring管理的单例
    public DataSource dataSource() {
        // 创建数据源
        return new HikariDataSource();
    }
}

// 替代方案2:使用静态类(如果没有状态)
public final class MathUtils {
    // 私有构造,防止实例化
    private MathUtils() {}
    
    public static double calculateCircleArea(double radius) {
        return Math.PI * radius * radius;
    }
}

// 替代方案3:使用枚举(Java枚举天然单例)
public enum ServiceLocator {
    INSTANCE;
    
    private Map<String, Object> services = new HashMap<>();
    
    public void register(String name, Object service) {
        services.put(name, service);
    }
    
    public Object getService(String name) {
        return services.get(name);
    }
}

五、面试常见问题与回答 💼

5.1 基础问题

Q1:单例模式是什么?

A:单例模式确保一个类只有一个实例,并提供全局访问点。它通过私有化构造函数、自行创建实例、提供静态访问方法来实现。

Q2:为什么要用单例模式?

A:主要为了:1) 控制资源访问(如数据库连接);2) 避免状态不一致;3) 节省系统资源;4) 提供全局访问点。

Q3:单例模式有哪些实现方式?

A:有6种主要实现:1) 饿汉式;2) 懒汉式;3) 同步方法懒汉式;4) 双重检查锁定;5) 静态内部类;6) 枚举式。

5.2 进阶问题

Q4:双重检查锁定中为什么要用volatile?

A:防止指令重排序。没有volatile时,instance = new Singleton()可能被重排序,导致其他线程拿到未完全初始化的对象。

Q5:如何防止单例被反射攻击?

A:在构造函数中添加检查,如果实例已存在则抛出异常。但最安全的方式是使用枚举单例。

Q6:如何防止单例被序列化破坏?

A:实现readResolve()方法返回单例实例,或使用枚举单例。

5.3 实战问题

Q7:Spring中的单例和设计模式的单例有什么区别?

A:Spring的单例是容器级别 的单例(每个容器一个实例),而设计模式的单例是JVM级别的单例(整个JVM一个实例)。

Q8:单例模式在集群环境下会有什么问题?

A:每个JVM都会有自己的单例实例,导致集群中多个实例。解决方案:使用分布式缓存、数据库或分布式锁来实现集群单例。

Q9:单例模式如何支持延迟加载?

A:使用懒汉式、双重检查锁定或静态内部类实现。静态内部类是最推荐的方式,它利用类加载机制保证线程安全和延迟加载。

六、总结与最佳实践 📚

6.1 核心要点回顾

  1. 单例模式的核心:保证一个类只有一个实例,全局访问
  2. 关键实现点:私有构造函数、静态实例、静态访问方法
  3. 推荐实现枚举单例 (防反射、序列化)或静态内部类(延迟加载、线程安全)
  4. 典型场景:配置管理、连接池、日志记录、缓存管理等需要全局唯一实例的场景

6.2 选择指南

6.3 最佳实践建议

  1. 优先考虑依赖注入:现代框架(如Spring)已经很好地管理了单例
  2. 谨慎使用单例:单例本质上是全局变量,可能带来隐藏的依赖和测试困难
  3. 考虑线程安全:多线程环境下必须保证线程安全
  4. 注意序列化 :如果需要序列化,实现readResolve()方法
  5. 避免单例持有大数据:单例生命周期长,持有大数据可能导致内存泄漏

6.4 实际项目建议

在实际项目中,我建议:

  1. 配置类使用单例 :如ConfigManager,确保配置一致性
  2. 资源管理类使用单例 :如ConnectionPool,避免资源浪费
  3. 工具类考虑静态方法:如果不需要状态,使用静态工具类更简单
  4. 使用框架管理 :在Spring项目中,使用@Component + @Scope("singleton")

🎯 最后的建议:单例模式是强大的工具,但也是双刃剑。正确使用可以提高代码质量和性能,滥用则可能导致代码难以测试和维护。理解其原理和适用场景,才能做出正确的设计决策。


参考资料 📖

  1. 设计模式:单例模式 | 菜鸟教程
  2. Java Singleton Design Pattern Best Practices
  3. Why ENUM Singleton is better in Java
  4. Spring Singleton vs Java Singleton

🔍 学习建议:理解单例模式后,尝试在个人项目中实践。可以从一个简单的配置管理器开始,然后逐步实现更复杂的场景如连接池。同时,阅读Spring等框架的源码,看看它们是如何实现和管理单例的,这是提升设计能力的好方法。


标签 : 单例模式 设计模式 创建型模式 Java设计模式 软件设计

互动环节:你在项目中是如何使用单例模式的?遇到过哪些问题或有独特的实践经验?欢迎在评论区分享你的单例模式实践故事!

相关推荐
癫狂的兔子2 小时前
【Python】【NumPy】学习笔记
python·学习·numpy
RealizeInnerSelf丶2 小时前
Web 网页如何唤起本地 Windows 应用并传递参数(含 Electron 自动注册 + 手动配置指南)
前端·windows
Kurbaneli2 小时前
Python的起源与发展
python
540_5402 小时前
ADVANCE Day26
人工智能·python·机器学习
dazzle2 小时前
Python高级技巧:装饰器全面指南,从基础到高级应用
python
南_山无梅落2 小时前
11.Python 常用数据类型「增删改查」操作总结表格
python
IT_陈寒2 小时前
Redis 性能优化实战:5个被低估的配置项让我节省了40%内存成本
前端·人工智能·后端
chilavert3182 小时前
技术演进中的开发沉思-261 Ajax:动画优化
前端·javascript·ajax
尘心cx2 小时前
前端-APIs-day3
开发语言·前端·javascript