单例模式是设计模式中最简单也最常用的模式之一,它确保一个类只有一个实例,并提供一个全局访问点。这在需要控制资源访问或共享状态的场景中非常有用。
文章目录
-
- [🌟 引言:一个真实世界的问题](#🌟 引言:一个真实世界的问题)
- 一、什么是单例模式?🎯
-
- [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 线程安全的懒汉式)
-
- 方式一:同步方法(效率低)
- [方式二:双重检查锁定(Double-Checked Locking)](#方式二:双重检查锁定(Double-Checked Locking))
- 方式三:静态内部类(推荐)
- [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 实际项目建议)
- [参考资料 📖](#参考资料 📖)
🌟 引言:一个真实世界的问题
想象一下这个场景:在一个大型电商系统中,配置管理器负责读取和管理所有系统配置。如果每个模块都自己创建配置管理器实例,会出现什么问题?
- 内存浪费:每个实例都加载一次配置文件,占用额外内存
- 配置不一致:不同实例可能有不同的配置状态
- 资源冲突:多个实例同时写入配置可能导致数据损坏
这就是单例模式要解决的核心问题 :确保一个类只有一个实例,并提供全局访问点。
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 核心要点回顾
- 单例模式的核心:保证一个类只有一个实例,全局访问
- 关键实现点:私有构造函数、静态实例、静态访问方法
- 推荐实现 :枚举单例 (防反射、序列化)或静态内部类(延迟加载、线程安全)
- 典型场景:配置管理、连接池、日志记录、缓存管理等需要全局唯一实例的场景
6.2 选择指南

6.3 最佳实践建议
- 优先考虑依赖注入:现代框架(如Spring)已经很好地管理了单例
- 谨慎使用单例:单例本质上是全局变量,可能带来隐藏的依赖和测试困难
- 考虑线程安全:多线程环境下必须保证线程安全
- 注意序列化 :如果需要序列化,实现
readResolve()方法 - 避免单例持有大数据:单例生命周期长,持有大数据可能导致内存泄漏
6.4 实际项目建议
在实际项目中,我建议:
- 配置类使用单例 :如
ConfigManager,确保配置一致性 - 资源管理类使用单例 :如
ConnectionPool,避免资源浪费 - 工具类考虑静态方法:如果不需要状态,使用静态工具类更简单
- 使用框架管理 :在Spring项目中,使用
@Component+@Scope("singleton")
🎯 最后的建议:单例模式是强大的工具,但也是双刃剑。正确使用可以提高代码质量和性能,滥用则可能导致代码难以测试和维护。理解其原理和适用场景,才能做出正确的设计决策。
参考资料 📖
- 设计模式:单例模式 | 菜鸟教程
- Java Singleton Design Pattern Best Practices
- Why ENUM Singleton is better in Java
- Spring Singleton vs Java Singleton
🔍 学习建议:理解单例模式后,尝试在个人项目中实践。可以从一个简单的配置管理器开始,然后逐步实现更复杂的场景如连接池。同时,阅读Spring等框架的源码,看看它们是如何实现和管理单例的,这是提升设计能力的好方法。
标签 : 单例模式 设计模式 创建型模式 Java设计模式 软件设计
互动环节:你在项目中是如何使用单例模式的?遇到过哪些问题或有独特的实践经验?欢迎在评论区分享你的单例模式实践故事!