单例模式是一种创建型设计模式,它保证一个类只有一个实例,并提供全局访问点来获取该实例。核心是通过私有化构造函数、静态方法和静态变量控制实例化过程。
解决的问题
- 资源重复创建问题(如数据库连接池、日志管理器)。
- 多个实例导致数据不一致问题(如配置管理器)。
特点
- 私有构造函数:禁止外部通过
new
创建实例。 - 静态实例:通过静态变量持有唯一实例。
- 延迟加载:部分实现中实例在首次访问时创建。
- 线程安全:多线程环境下需保证实例唯一性。
编码实现
基础版(线程不安全):
public class Singleton {
private static Singleton instance;
private Singleton() {} // 私有构造
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
线程安全版(双重检查锁):
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
关键点:volatile关键字,修饰的变量不允许线程内部缓存和重排序,双重检查减少锁竞争。
补充:
-
Java 内存模型定义了线程如何与主内存(Main Memory)和本地工作内存(Working Memory)交互。每个线程都有一个独立的工作内存
-
线程内部缓存(本地工作内存):每个线程在运行时会有一个独立的工作内存(线程本地缓存),用于存储从主内存中拷贝的变量副本。
-
主内存(Main Memory):存储所有共享变量(如堆中的对象、静态变量)。
-
线程对变量的操作(读取或修改)通常直接作用于工作内存中的副本,而非直接操作主内存中的变量。
实践应用
场景1:配置管理器
- 需求:全局共享配置信息,避免重复读取配置文件。
- 实现:
public class ConfigManager {
private static volatile ConfigManager instance;
private Properties configs = new Properties();
private ConfigManager() {
// 加载配置文件
try (InputStream input = getClass().getResourceAsStream("app.properties")) {
configs.load(input);
} catch (IOException e) {
e.printStackTrace();
}
}
public static ConfigManager getInstance() {
if (instance == null) {
synchronized (ConfigManager.class) {
if (instance == null) {
instance = new ConfigManager();
}
}
}
return instance;
}
public String getConfig(String key) {
return configs.getProperty(key);
}
}
场景2:日志记录器
- 需求:统一管理日志输出,避免多个日志实例竞争文件句柄。
- 实现:通过单例模式封装日志工具(如Log4j2的LoggerContext)。
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.LoggerContext;
import java.util.Objects;
public class LogSingleton {
// 1. 静态私有实例 + volatile保证可见性
private static volatile LogSingleton instance;
// 2. Log4j2的LoggerContext(核心日志上下文)
private LoggerContext loggerContext;
// 3. 实际日志记录工具
private Logger logger;
// 4. 私有构造函数(初始化日志配置)
private LogSingleton() {
// 加载Log4j2配置(默认加载类路径下的log4j2.xml)
loggerContext = (LoggerContext) LogManager.getContext(false);
logger = loggerContext.getLogger(LogSingleton.class.getName());
logger.info("LogSingleton初始化完成");
}
// 5. 双重检查锁获取单例
public static LogSingleton getInstance() {
if (instance == null) {
synchronized (LogSingleton.class) {
if (instance == null) {
instance = new LogSingleton();
}
}
}
return instance;
}
// 6. 提供日志记录方法(委托给Log4j2的Logger)
public void info(String message) {
logger.info(message);
}
public void error(String message, Throwable throwable) {
logger.error(message, throwable);
}
// 7. 可选:重新加载配置文件(例如动态修改日志级别)
public void reloadConfiguration() {
if (loggerContext != null) {
loggerContext.setConfigLocation(Objects.requireNonNull(
getClass().getClassLoader().getResource("log4j2.xml")).toURI());
logger.info("日志配置已重新加载");
}
}
}
类和对象的关系(UML类图)
+------------------+
| Singleton |
+------------------+
| - instance: Singleton |
+------------------+
| - Singleton() |
| + getInstance(): Singleton |
+------------------+
- Singleton类:包含一个静态私有变量
instance
,私有构造方法,公有静态方法getInstance()
。
- 对象关系:通过
getInstance()
方法控制唯一实例的创建和访问。
我的总结
单例模式(singleton)
目的:使得全局只有一个实例
应用场景:读取配置、打印日志等功能类(需要全局唯一实例)
实现 - 类和对象的关系(UML类图):
+--------------------------------------------------------+
| Singleton |
+--------------------------------------------------------+
| - private static volatile instance: Singleton |
+--------------------------------------------------------+
| - private xx:int #属性1 |
| - private xx:int #属性2 |
+--------------------------------------------------------+
| - Singleton() #构造器:给普通属性赋值 |
| + getInstance(): Singleton #调用构造器,给静态属性赋值 |
+--------------------------------------------------------+
| + method1(): Singleton |
| + method2(): int |
+--------------------------------------------------------+
- 对象关系:通过
getInstance()
方法控制唯一实例的创建和访问。(线程安全:双重检查 + synchronized 关键字)
适用性:单例模式适合需要全局唯一对象且频繁访问的场景。
注意事项:
- 多线程环境下必须保证线程安全。
- 反射和序列化可能破坏单例,需额外防护(如
readResolve()
方法)。 - 过度使用单例可能导致代码耦合度高,测试困难。
补充:
上述单例模式也就是所谓的懒汉式,即在第一次调用getInstance()时才进行实例化
所谓的饿汉式则是在类初始化时,就已经完成实例化,具体如下
//饿汉式单例类.在类初始化时,已经自行实例化
public class Singleton1 {
private Singleton1() {}
private static final Singleton1 single = new Singleton1();
//静态工厂方法
public static Singleton1 getInstance() {
return single;
}
}