设计模式之单例模式
单例模式确保一个类只有一个实例,并提供一个全局访问点。
咋一看这个定义,先解决一个疑惑,什么时候需要保证类只有一个实例。在实际生产场景中,有许多这样的实例,大多出于两个目的,防止资源浪费和保证数据一致性。比如常见的数据库连接池、线程池、日志记录器、缓存实例、配置信息实例等通常只需要一个实例来管理资源,能够节省开销,避免多实例带来的数据不一致。
明确了实际用处后,再来解析定义:
- “确保一个类只有一个实例”,类是如何实例化的,通过调用构造函数,谁调动构造函数谁就能创建实例,确保只有一个实例,就是要控制构造函数被调用的权限,防止外部代码通过构造函数或其他方式创建多个实例。
- 私有化构造函数:将类的构造函数设为私有(或受保护),禁止外部直接调用构造函数创建实例。构造函数私有了,构造出的唯一实例也必须是本类自身维护了。
- 控制实例化过程:在类内部管理实例的创建,确保只有一个实例存在。
- “全局访问点”,为了让外部代码能够方便地获取这个唯一的实例,使用静态方法即可。

log(message)
,用于记录日志。import java.io.FileWriter;
import java.io.IOException;
public class Logger {
// 私有静态变量,存储唯一实例
private static Logger instance;
private String logFile = "app.log"; // 日志文件
// 私有构造函数,禁止外部直接创建实例
private Logger() {
if (instance != null) {
throw new IllegalStateException("Logger 是单例类,不能直接创建实例!");
}
}
public static Logger getInstance() {
instance = new Logger();
return instance;
}
// 记录日志的方法
public void log(String message) {
try (FileWriter writer = new FileWriter(logFile, true)) {
writer.write(message + "\n"); // 将日志信息写入文件
} catch (IOException e) {
e.printStackTrace();
}
}
}
在这个实现中,对定义中解析的重点都遵循到了,但从更严格的生产环境来看,存在线性安全问题,当多线程同时调用getInstance时,会有几率创建多个Logger实例。为什么?问题就出这两句中,如a线程都运行到【1】处,但还未到【2】处,此时b线程也运行到【1】处,也会判断instance == null,创建第二个Logger实例,导致多个实例,不符合单例要求。
if (instance != null) { 【1】
throw new IllegalStateException("Logger 是单例类,不能直接创建实例!");
}
【2】
增加线程安全控制,利用sychronized控制并发。
import java.io.FileWriter;
import java.io.IOException;
public class Logger {
// 私有静态变量,存储唯一实例
private static Logger instance;
private String logFile = "app.log"; // 日志文件
// 私有构造函数,禁止外部直接创建实例
private Logger() {
if (instance != null) {
throw new IllegalStateException("Logger 是单例类,不能直接创建实例!");
}
}
// 公有静态方法,提供全局访问点
public static Logger getInstance() {
if (instance == null) {
synchronized (Logger.class) { // 线程安全
if (instance == null) {
instance = new Logger(); // 创建唯一实例
}
}
}
return instance;
}
// 记录日志的方法
public void log(String message) {
try (FileWriter writer = new FileWriter(logFile, true)) {
writer.write(message + "\n"); // 将日志信息写入文件
} catch (IOException e) {
e.printStackTrace();
}
}
}
上述实现只有在真实调用的时候,才会创建实例,称为“懒汉式”。synchronized是一个重量级操作,每次调用 getInstance()
时都加锁对性能消耗大,因此做了双重检查锁定。
有“懒汉式”,就有一个与之对应的“饿汉式”,是指在类加载时就创建单例实例。这种方式避免了线程安全问题,但无法实现懒加载。
public class Singleton {
private static final Singleton instance = new Singleton(); // 类加载时创建实例
private Singleton() {
// 私有构造函数
}
public static Singleton getInstance() {
return instance;
}
}
这种实现简单,线程安全。但即使不需要使用单例实例,也会在类加载时创建,可能导致资源浪费。
还有一种静态内部类方式结合了懒汉式和饿汉式的优点,既实现了懒加载,又保证了线程安全。
public class Singleton {
private Singleton() {
// 私有构造函数
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton(); // 静态内部类中创建实例
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE; // 第一次调用时加载静态内部类
}
}
静态内部类方式,实现了懒加载:只有在第一次调用 getInstance()
时,才会加载 SingletonHolder
类并创建实例。
单例模式的三种实现方式各有优缺点,适用于不同的场景:
- 懒汉式:适合需要懒加载的场景,但需要考虑线程安全问题。
- 饿汉式:适合单例实例创建成本低且不需要懒加载的场景。
- 静态内部类:结合了懒汉式和饿汉式的优点,是最推荐的方式。
不管哪一种方式,都遵循开篇对定义解析的重点,构造函数私有化,通过静态函数提供外部访问,不同的在于实例创建的时机。