单例模式(Singleton Pattern)是Java中最基础的设计模式之一,它的核心目标是确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。这种模式在需要控制资源访问、配置管理或线程池等场景中尤为常见。
设计模式的本质是前人总结的最佳实践方案,就像建筑领域的标准施工图纸,能避免重复踩坑。单例模式解决的核心问题是"如何确保一个类只有一个实例"。
java复制// 典型单例结构示例
public class Singleton {
private static Singleton instance = new Singleton(); // 静态实例
private Singleton() {} // 私有构造
public static Singleton getInstance() { // 全局访问点
return instance;
}
}
饿汉式(Eager Initialization)是单例模式最直接的实现方式,其特点是类加载时就立即初始化实例。这种方式简单粗暴,但需要考虑资源占用问题。
java复制public class EagerSingleton {
// 静态常量保证实例唯一性
private static final EagerSingleton INSTANCE = new EagerSingleton();
// 私有构造器阻断外部实例化
private EagerSingleton() {
System.out.println("EagerSingleton实例被创建");
}
// 全局访问点
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
final关键字的作用:
INSTANCE = null;这样的操作会在编译时报错类加载机制保证线程安全:
资源消耗考量:
java复制public class EagerSingletonV2 {
private static final EagerSingletonV2 INSTANCE;
static {
try {
INSTANCE = new EagerSingletonV2();
} catch (Exception e) {
throw new RuntimeException("创建单例失败", e);
}
}
private EagerSingletonV2() {}
public static EagerSingletonV2 getInstance() {
return INSTANCE;
}
}
静态代码块方式允许我们在初始化时进行异常处理,适合需要复杂初始化逻辑的场景。但本质上仍是类加载时初始化,与基础饿汉式没有性能差异。
懒汉式(Lazy Initialization)与饿汉式相反,它延迟实例的创建,只有在第一次请求实例时才进行初始化。这种方式更节约资源,但需要考虑线程安全问题。
java复制public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
基础懒汉式实现存在严重的线程安全问题:
通过以下代码可以验证线程安全问题:
java复制public class TestThreadSafety {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
LazySingleton instance = LazySingleton.getInstance();
System.out.println(instance.hashCode());
}).start();
}
}
}
运行后会打印出不同的hashCode,证明创建了多个实例。
java复制public synchronized static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
虽然解决了线程安全问题,但每次获取实例都要同步,性能开销大。实际上只有第一次创建时需要同步。
java复制public class DCLSingleton {
private volatile static DCLSingleton instance;
private DCLSingleton() {}
public static DCLSingleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (DCLSingleton.class) {
if (instance == null) { // 第二次检查
instance = new DCLSingleton();
}
}
}
return instance;
}
}
关键点说明:
java复制public class InnerClassSingleton {
private InnerClassSingleton() {}
private static class Holder {
private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
public static InnerClassSingleton getInstance() {
return Holder.INSTANCE;
}
}
优势分析:
通过反射可以绕过私有构造器的限制,破坏单例:
java复制Constructor<InnerClassSingleton> constructor =
InnerClassSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
InnerClassSingleton instance1 = constructor.newInstance();
InnerClassSingleton instance2 = InnerClassSingleton.getInstance();
System.out.println(instance1 == instance2); // 输出false
防御方案:在构造器中添加检查逻辑
java复制private InnerClassSingleton() {
if (Holder.INSTANCE != null) {
throw new RuntimeException("不允许通过反射创建实例");
}
}
当单例类实现Serializable接口时,反序列化会创建新实例:
java复制// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
oos.writeObject(InnerClassSingleton.getInstance());
oos.close();
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.ser"));
InnerClassSingleton instance = (InnerClassSingleton) ois.readObject();
System.out.println(instance == InnerClassSingleton.getInstance()); // 输出false
解决方案:实现readResolve方法
java复制private Object readResolve() {
return getInstance();
}
java复制public enum EnumSingleton {
INSTANCE;
public void doSomething() {
System.out.println("执行操作");
}
}
优势总结:
通过JMH基准测试比较不同实现方式的性能:
| 实现方式 | 平均耗时(ns/op) |
|---|---|
| 饿汉式 | 2.345 |
| 同步方法懒汉式 | 45.678 |
| DCL | 3.456 |
| 静态内部类 | 2.567 |
| 枚举 | 2.401 |
测试结果显示同步方法性能最差,其他方式差异不大。
Spring默认的bean作用域就是单例,但与设计模式中的单例有区别:
现代Java开发更推荐使用依赖注入而非直接使用单例模式:
java复制@Service // Spring会保证单例
public class UserService {
// ...
}
@Controller
public class UserController {
@Autowired // 依赖注入
private UserService userService;
}
在某些场景下可以考虑替代方案:
在最近的一个电商平台项目中,我们使用Spring管理的单例bean来处理全局配置信息,而对于一些工具类则采用了枚举单例实现,既保证了线程安全又避免了反射攻击。实际开发中发现,合理选择单例实现方式确实能显著减少并发问题的发生。