Java单例模式:预加载与延迟初始化的实现
大家好!今天就结合自己的学习经历,来和大家分享一下Java中单例模式的两种经典实现方式。文章里会包含我踩过的坑、学习时的困惑,以及如何正确使用它们。
一、先搞懂:单例模式的核心设计思路
作为一个初学者,我一开始也搞不懂"单例"到底是什么。简单来说,单例模式就是确保一个类在整个应用中只创建一个对象。就像我们班只有一个班长,不会有两个班长一样。
要实现单例模式,无论哪种写法,都离不开三个核心步骤(这是我总结的"万能公式"):
- 私有化构造器:禁止外部通过
new关键字创建对象 - 定义静态类变量:存储该类的唯一实例(静态变量属于类级别,不是每个对象都有)
- 提供公开静态方法:对外暴露获取唯一实例的入口
二、预加载单例:"类加载时就创建实例"的实现
预加载单例的特点是:类加载时就创建好实例,后续获取直接返回。就像我提前把早餐准备好,早上直接吃就行。
1. 代码实现
package com.wmh.singleinstance;
// 预加载单例类
public class A {
// 静态变量存储唯一实例:类加载时直接初始化
private static A a = new A();
// 私有化构造器:外部无法通过 new 创建对象
private A() {}
// 公开静态方法:返回唯一实例
public static A getInstance() {
return a;
}
}
2. 为什么叫"预加载"?
我一开始也很好奇为什么叫"预加载"。后来我理解了:预加载单例在类加载时就创建了实例,不需要等待。这就像我提前把早餐准备好,早上直接吃就行。
3. 优点与缺点
- 优点:实现简单、天然线程安全(类加载过程本身就是线程安全的)
- 缺点:如果类加载后长期不使用该实例,会造成内存浪费(比如一个工具类,可能很少用到)
三、延迟初始化单例:"需要时才创建实例"的实现
延迟初始化单例的特点是:只有在第一次获取实例时才创建对象。就像我这种懒人,不到饿得不行,是不想做饭的。
1. 代码实现
package com.wmh.singleinstance;
// 延迟初始化单例类
public class B {
// 静态变量暂存实例:初始为 null,不提前创建
private static B b;
// 私有化构造器:外部无法通过 new 创建对象
private B() {}
// 公开静态方法:第一次调用时才创建实例
public static B getInstance() {
if (b == null) {
// 首次获取时创建对象
b = new B();
}
return b;
}
}
2. 重要提醒:延迟初始化单例不安全!
这是我在学习过程中踩过的大坑!基础版的延迟初始化单例不是线程安全的。如果在多线程环境下,两个线程同时判断 b == null,都可能创建实例,导致多个对象。
四、测试验证:确保实例唯一
我写了一个简单的测试类来验证两种单例是否真的只有一个实例:
1. 测试代码
package com.wmh.singleinstance;
public class Test {
public static void main(String[] args) {
// 测试预加载单例
A a1 = A.getInstance();
A a2 = A.getInstance();
System.out.println("预加载单例:a1 == a2 ? " + (a1 == a2));
System.out.println("a1 地址:" + a1);
System.out.println("a2 地址:" + a2);
// 测试延迟初始化单例
B b1 = B.getInstance();
B b2 = B.getInstance();
System.out.println("\n延迟初始化单例:b1 == b2 ? " + (b1 == b2));
System.out.println("b1 地址:" + b1);
System.out.println("b2 地址:" + b2);
}
}
2. 运行结果
预加载单例:a1 == a2 ? true
a1 地址:com.wmh.singleinstance.A@1b6d3586
a2 地址:com.wmh.singleinstance.A@1b6d3586
延迟初始化单例:b1 == b2 ? true
b1 地址:com.wmh.singleinstance.B@4554617c
b2 地址:com.wmh.singleinstance.B@4554617c
3. 我的思考
看到 a1 == a2 和 b1 == b2 都是 true,我松了口气,说明两种实现都达到了单例的目的。但我也明白,如果在多线程环境下,基础版延迟初始化单例可能不安全。
五、单例模式的进阶:枚举类实现单例
在学习过程中,我发现了一个更优雅的单例实现方式——使用枚举类!这在上传的文档中也有提到。
1. 枚举类实现单例
package com.wmh.enumdemo;
public enum SingletonEnum {
INSTANCE;
// 可以添加一些方法
public void doSomething() {
System.out.println("单例枚举方法");
}
}
2. 为什么枚举类实现单例这么棒?
- 天然单例:枚举类本身就是单例的,无需额外处理
- 线程安全:枚举类的实例化是线程安全的
- 防止反射破坏:枚举类可以防止通过反射创建多个实例
- 防止序列化破坏:枚举类可以防止序列化后创建多个实例
3. 使用方式
// 获取单例
SingletonEnum instance = SingletonEnum.INSTANCE;
instance.doSomething();
六、延迟初始化单例线程安全优化方案
基础版延迟初始化单例不安全,但有几种优化方案:
1. 加 synchronized 关键字(简单但效率低)
public static synchronized B getInstance() {
if (b == null) {
b = new B();
}
return b;
}
2. 双重检查锁定(DCL,推荐)
private static volatile B b;
public static B getInstance() {
if (b == null) {
synchronized (B.class) {
if (b == null) {
b = new B();
}
}
}
return b;
}
💡 小提示:
volatile关键字是关键,它可以防止指令重排序,确保在多线程环境下安全。
七、单例模式的应用场景
作为一个初学者,我一开始不太明白单例模式有什么用。后来通过学习,我发现它在实际开发中非常常见:
- Java中的 Runtime 类:
Runtime.getRuntime()返回唯一实例 - 数据库连接池:避免创建过多连接导致资源耗尽
- 配置管理类:全局只需一个配置对象
- 日志管理器:确保日志记录的一致性
八、总结与选择建议
1. 三种单例实现对比
| 实现方式 | 简单性 | 线程安全 | 优点 | 缺点 |
|---|---|---|---|---|
| 预加载单例 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 简单、线程安全 | 可能浪费内存 |
| 延迟初始化单例(基础) | ⭐⭐⭐ | ⭐ | 节省内存 | 不安全 |
| 延迟初始化单例(DCL) | ⭐⭐ | ⭐⭐⭐⭐⭐ | 节省内存、线程安全 | 代码稍复杂 |
| 枚举类 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 优雅、安全、防止反射破坏 | 不能延迟加载 |
2. 我的建议
- 如果实例创建成本低、且肯定会用到,用预加载单例(简单、安全)
- 如果实例创建成本高、可能长期不用,用枚举类(最推荐!)
- 如果必须用延迟初始化单例,一定要用DCL方案(双重检查锁定)
九、我的学习心得
作为一个Java初学者,我最开始被单例模式搞得很懵。后来我总结了几点经验:
- 不要在多线程环境下使用基础版延迟初始化单例!这是初学者最容易犯的错误
- 枚举类实现单例是最优雅的方式,我强烈推荐在新项目中使用
- 理解原理比死记硬背更重要,我花了很多时间理解为什么延迟初始化单例需要双重检查
十、写在最后
单例模式是设计模式的入门经典,掌握它不仅能解决实际开发中的资源控制问题,更能培养"最优解思维"。希望这篇文章能帮助到和我一样的初学者。
如果你也是Java初学者,欢迎在评论区交流:你在项目中用过哪种单例实现?有没有遇到过坑?我特别想听听你的故事!
如果觉得这篇文章对你有帮助,别忘了点赞、收藏、转发给身边的Java小伙伴!😊