设计模式入门与单例模式 | JavaSE
设计模式入门与单例模式
一、学习目标
学完本章,你应该能够:
- 解释什么是设计模式(Design Pattern)。
- 理解设计模式不是固定代码模板,而是针对重复设计问题总结出的可复用解决思路。
- 知道学习一个设计模式至少应该关注“解决什么问题、核心结构、如何使用、优缺点和适用场景”。
- 理解什么是单例模式(Singleton Pattern)。
- 解释为什么单例类通常需要私有构造器。
- 独立手写饿汉式单例。
- 独立手写基础懒汉式单例。
- 区分饿汉式与懒汉式的对象创建时机。
- 解释为什么多次调用
getInstance()应得到同一个对象。 - 理解基础懒汉式在多线程环境中可能存在的问题。
- 理解“单例”并不等于“代码中所有东西都应该 static”。
- 初步理解单例模式的优点、限制和适用场景。
二、核心知识
2.1 什么是设计模式
原始入门教材经常将设计模式概括为:
一个问题有很多解决方法,人们把其中优秀的解决方案总结下来,就形成了设计模式。
这个描述非常适合建立第一印象。
更准确地说:
设计模式是针对软件设计中反复出现的一类问题,在特定上下文中总结出的、经过实践验证的可复用设计方案。
它不是:
一道题唯一正确的标准答案
也不是:
复制一段固定代码就结束
而更像:
重复出现的设计问题
↓
分析对象之间的职责与关系
↓
形成可复用的结构思路
↓
根据具体业务进行实现
所以设计模式的核心仍然是:
设计。
2.2 为什么需要设计模式
假设软件开发过程中反复出现:
如何保证某种对象只创建一次?
如何把固定流程和可变步骤分开?
如何让不同实现可以灵活替换?
如何创建复杂对象?
如何通知多个观察者状态发生变化?
这些问题并不是某一个项目独有。
大量开发者长期实践以后,就可能总结出相对成熟的解决思路。
例如:
一个类只应该提供一个共享实例
↓
Singleton
固定算法骨架,把部分步骤交给子类
↓
Template Method
本章先学习:
Singleton Pattern —— 单例模式
后面的:
02-16 模板方法设计模式
还会继续学习另一个设计模式。
2.3 学设计模式到底学什么
不要把设计模式学习成:
背代码
至少应该回答几个问题:
1. 它解决什么问题?
2. 为什么需要它?
3. 核心结构是什么?
4. 怎么实现?
5. 在什么场景使用?
6. 有什么优点?
7. 有什么代价和限制?
例如学习单例模式:
问题:
为什么这个类不应该随便创建多个对象?
结构:
private constructor
+
static instance
+
public static getInstance()
区别:
什么时候创建对象?
风险:
共享可变状态、线程安全、耦合等
真正做到这些,才不是“只背模板”。
2.4 什么是单例模式
单例模式英文:
Singleton Pattern
当前阶段可以将它理解成:
控制某个类的实例创建,使程序按照设计只对外提供该类的一个共享实例,并提供统一的获取入口。
最直观的目标:
外部不能:
new Singleton()
new Singleton()
new Singleton()
而只能:
Singleton.getInstance()
并且每次获取到:
同一个对象
2.5 为什么有些类只需要一个对象
并不是所有类都应该只有一个对象。
例如:
Student
User
Order
Article
BankAccount
显然可能需要:
很多个对象
但是某些对象表达的是:
应用级统一配置
统一资源协调器
任务管理器
运行时环境对象
某些共享服务对象
在某个设计中可能只希望存在一份共享实例。
例如:
ApplicationConfig
如果系统中不同位置各创建:
config1
config2
config3
而它们又分别保存不同状态,就可能破坏:
系统配置只有一个权威来源
这一设计。
这时可以考虑单例。
2.6 普通类为什么无法保证只创建一个对象
普通类:
public class AppConfig {
public AppConfig() {
}
}
外部可以:
AppConfig c1 = new AppConfig();
AppConfig c2 = new AppConfig();
AppConfig c3 = new AppConfig();
结果:
c1
c2
c3
引用的是不同对象。
如果我们希望:
外部不能随意创建对象。
第一步应该控制什么?
答案就是:
构造器。
2.7 单例第一步:私有化构造器
修改:
public class AppConfig {
private AppConfig() {
}
}
现在外部不能:
AppConfig config =
new AppConfig();
因为构造器:
private AppConfig()
只允许当前类内部直接使用。
这就是单例模式非常关键的第一步:
控制对象创建入口。
2.8 私有构造器之后又出现新问题
虽然:
private AppConfig() {
}
阻止了外部:
new AppConfig();
但同时外部也完全拿不到:
AppConfig 对象
怎么办?
既然类内部可以调用自己的私有构造器,就可以:
public class AppConfig {
private AppConfig() {
}
private static AppConfig instance =
new AppConfig();
}
类内部创建一份对象,然后保存起来。
为什么使用:
static
因为:
还没有 AppConfig 对象时,就必须有办法获取这一共享实例。
所以共享实例本身通常属于:
类级成员
2.9 第三步:提供统一获取方法
外部仍然不能直接访问:
private static AppConfig instance;
因此还需要:
public static AppConfig getInstance() {
return instance;
}
完整结构:
public class AppConfig {
private static final AppConfig INSTANCE =
new AppConfig();
private AppConfig() {
}
public static AppConfig getInstance() {
return INSTANCE;
}
}
外部:
AppConfig c1 =
AppConfig.getInstance();
AppConfig c2 =
AppConfig.getInstance();
此时:
System.out.println(c1 == c2);
应该观察到:
true
因为:
c1
c2
引用的是同一个对象。
2.10 单例模式的三块核心结构
初学阶段先牢牢记住:
1. private 构造器
↓
阻止外部随意 new
2. static 实例字段
↓
类自己保存实例
3. public static getInstance()
↓
向外提供统一获取入口
可以画成:
外部代码
│
│ getInstance()
▼
┌────────────────┐
│ Singleton 类 │
│ │
│ private 构造器 │
│ │ │
│ ▼ │
│ 唯一共享实例 │
└────────────────┘
2.11 什么是饿汉式单例
饿汉式英文经常称:
Eager Initialization Singleton
特点:
类初始化过程中就创建好实例,等调用者来获取时,对象已经存在。
例如:
public final class AppConfig {
private static final AppConfig INSTANCE =
new AppConfig();
private AppConfig() {
}
public static AppConfig getInstance() {
return INSTANCE;
}
}
关键就在:
private static final AppConfig INSTANCE =
new AppConfig();
实例字段初始化时:
对象已经创建
之后:
AppConfig.getInstance()
只负责:
返回已有对象
2.12 为什么叫“饿汉式”
可以用一个非常形象的比喻:
不管你现在用不用
我先把饭准备好
所以:
类准备好
↓
实例也准备好
调用:
getInstance()
时无需再判断:
有没有实例?
直接:
return INSTANCE;
即可。
2.13 饿汉式完整案例
public final class TaskManager {
private static final TaskManager INSTANCE =
new TaskManager();
private TaskManager() {
System.out.println(
"TaskManager 创建"
);
}
public static TaskManager getInstance() {
return INSTANCE;
}
public void executeTask() {
System.out.println(
"执行任务"
);
}
}
测试:
public class Test {
public static void main(String[] args) {
TaskManager t1 =
TaskManager.getInstance();
TaskManager t2 =
TaskManager.getInstance();
System.out.println(t1 == t2);
t1.executeTask();
}
}
核心验证不是:
对象有没有创建
而是:
多次获取是不是同一个对象。
2.14 为什么实例字段推荐 private
不要:
public static final AppConfig INSTANCE =
new AppConfig();
虽然这种写法也能够直接访问实例,但传统单例实现更常通过:
private static final AppConfig INSTANCE
配合:
public static AppConfig getInstance()
这样可以:
- 封装内部创建方式;
- 统一对象获取入口;
- 将来修改实现时减少调用方依赖;
- 更明确地表达单例 API。
所以本章统一采用:
private instance
+
public getInstance()
2.15 什么是懒汉式单例
懒汉式英文通常称:
Lazy Initialization Singleton
特点:
真正第一次需要对象时才创建实例。
例如:
public final class AppConfig {
private static AppConfig instance;
private AppConfig() {
}
public static AppConfig getInstance() {
if (instance == null) {
instance =
new AppConfig();
}
return instance;
}
}
类刚初始化时:
instance = null
第一次:
AppConfig.getInstance()
发现:
instance == null
于是:
new AppConfig()
以后再次调用:
instance != null
直接返回已经存在的对象。
2.16 为什么叫“懒汉式”
可以理解成:
你不找我要
我就不创建
流程:
第一次 getInstance()
↓
instance == null ?
↓ 是
new Singleton()
↓
保存实例
↓
return instance
第二次 getInstance()
↓
instance == null ?
↓ 否
直接 return
这就是:
延迟初始化(Lazy Initialization)
2.17 懒汉式完整案例
public final class CacheManager {
private static CacheManager instance;
private CacheManager() {
System.out.println(
"CacheManager 创建"
);
}
public static CacheManager getInstance() {
if (instance == null) {
instance =
new CacheManager();
}
return instance;
}
public void clear() {
System.out.println(
"清理缓存"
);
}
}
测试:
public class Test {
public static void main(String[] args) {
CacheManager c1 =
CacheManager.getInstance();
CacheManager c2 =
CacheManager.getInstance();
System.out.println(c1 == c2);
}
}
在单线程调用中,可以看到两次返回同一个引用。
2.18 饿汉式与懒汉式对比
| 对比 | 饿汉式 | 基础懒汉式 |
| ------------------------------ | ------------------------------ | ---------------- |
| 对象创建时间 | 类初始化阶段 | 第一次真正获取时 |
| 是否延迟创建 | 否 | 是 |
| getInstance() | 直接返回 | 先判断是否为空 |
| 代码复杂度 | 更简单 | 稍复杂 |
| 基础实现多线程风险 | JVM 类初始化机制保证初始化过程 | 有竞争条件 |
| 如果实例创建成本很高且可能不用 | 可能提前创建 | 可以推迟创建 |
最重要的区别:
饿汉:
先创建,再等别人获取
懒汉:
别人第一次获取时再创建
2.19 基础懒汉式为什么存在多线程问题
当前代码:
public static AppConfig getInstance() {
if (instance == null) {
instance =
new AppConfig();
}
return instance;
}
单线程执行通常没有问题。
但是假设未来存在:
线程 A
线程 B
它们几乎同时进入:
if (instance == null)
可能发生:
线程 A:
看到 instance == null
线程 B:
也看到 instance == null
线程 A:
new AppConfig()
线程 B:
也 new AppConfig()
于是一次竞争过程中可能创建:
不止一个对象
这就破坏了单例设计意图。
2.20 本章暂时不解决线程安全懒汉式
解决这一问题会涉及:
线程
竞态条件
synchronized
volatile
内存可见性
双重检查锁
类初始化
这些都属于后面的:
GROUP 08 · 多线程与并发
所以本章严格按照当前知识边界:
只要求你发现基础懒汉式存在多线程安全风险,不要求现在实现复杂线程安全版本。
等学习多线程以后,可以再回来重新实现一次。
三、使用方法
3.1 饿汉式标准训练模板
public final class Singleton {
private static final Singleton INSTANCE =
new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return INSTANCE;
}
}
验证:
Singleton s1 =
Singleton.getInstance();
Singleton s2 =
Singleton.getInstance();
Singleton s3 =
Singleton.getInstance();
System.out.println(s1 == s2);
System.out.println(s2 == s3);
学习时不要只复制。
关闭 IDE 提示,从零写出:
私有构造器
静态实例
公开静态获取方法
3.2 基础懒汉式训练模板
public final class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance =
new Singleton();
}
return instance;
}
}
闭卷时必须能够解释:
if (instance == null)
为什么存在。
3.3 Runtime:JDK 中可以观察到的单实例设计
Java 自带:
Runtime.getRuntime()
例如:
Runtime r1 =
Runtime.getRuntime();
Runtime r2 =
Runtime.getRuntime();
System.out.println(r1 == r2);
Runtime 表示当前 Java 应用的运行环境对象。
调用者不能:
new Runtime();
而通过:
Runtime.getRuntime()
获取当前运行时对象。
这是理解:
控制构造
+
统一获取入口
非常好的 JDK 实例。
3.4 应用配置案例
例如:
public final class ApplicationConfig {
private static final ApplicationConfig INSTANCE =
new ApplicationConfig();
private String siteName =
"星雨笔录";
private int pageSize =
20;
private ApplicationConfig() {
}
public static ApplicationConfig getInstance() {
return INSTANCE;
}
public String getSiteName() {
return siteName;
}
public int getPageSize() {
return pageSize;
}
}
获取:
ApplicationConfig config =
ApplicationConfig.getInstance();
不同业务代码共享:
同一个 ApplicationConfig 实例
四、原理与进阶
4.1 单例模式真正解决的不是“少占一点内存”
原始入门资料常把单例优点描述为:
只创建一个对象,可以避免浪费内存。
这可以作为其中一个结果。
但真正核心的问题是:
某个业务概念是否应该存在唯一、统一、受控的共享实例。
例如:
系统统一配置
如果出现三个不同配置对象:
configA.pageSize = 20
configB.pageSize = 50
configC.pageSize = 100
真正危险的往往不是:
多占用了三个对象的内存
而是:
系统到底应该相信哪一个状态?
因此单例首先是:
对象生命周期与实例数量控制问题
而不是单纯的内存优化技巧。
4.2 单例并不意味着所有成员都 static
错误设计:
public class UserManager {
public static String username;
public static int age;
public static void login() {
}
}
然后说:
这就是单例。
不是。
这是:
大量静态成员
并没有真正建立一个单例对象。
单例模式仍然存在一个正常对象:
UserManager instance
然后通过:
UserManager.getInstance()
获得它。
对象内部完全可以拥有:
实例变量
实例方法
例如:
public final class UserManager {
private static final UserManager INSTANCE =
new UserManager();
private int onlineUserCount;
private UserManager() {
}
public static UserManager getInstance() {
return INSTANCE;
}
public void userLogin() {
onlineUserCount++;
}
public int getOnlineUserCount() {
return onlineUserCount;
}
}
所以:
单例是“一个对象”,不是“不要对象,全部 static”。
4.3 饿汉式为什么天然没有基础懒汉式的创建竞争
饿汉式:
private static final Singleton INSTANCE =
new Singleton();
实例在类初始化期间创建。
JVM 对类初始化过程本身有同步规则:
同一个 Class 对象的初始化过程
受到 JVM 初始化锁协调
因此这种简单饿汉式不会出现:
两个普通业务线程同时进入 getInstance()
然后各自 new 一次
的问题。
而基础懒汉式把:
if (instance == null)
放在普通业务方法执行过程中,自然产生并发竞争的可能。
4.4 单例也有代价
不要形成:
“只要全局都要用,就全部写成 Singleton。”
单例可能带来:
- 全局共享状态;
- 隐藏依赖;
- 类之间耦合增强;
- 单元测试相互影响;
- 生命周期控制变复杂;
- 并发修改共享状态时产生线程安全问题。
例如:
GlobalManager.getInstance()
在几十个类中到处出现。
表面上很方便。
但以后很难快速看出:
谁修改了里面的状态?
谁依赖它?
测试之间是否共享旧数据?
所以:
设计模式不是越多越高级,而是有问题时选择合适方案。
4.5 “只能创建一个对象”存在工程边界
当前学习阶段我们把单例理解成:
一个类对外只提供一个共享实例。
这是正确的主线。
但是严格工程环境中还存在:
多个 ClassLoader
反射
序列化
分布式多进程
等更复杂问题。
例如:
一个 JVM 中的一个类实例
和:
整个分布式系统世界上只能有一个实例
根本不是同一个问题。
本章不展开这些高级情况,但需要知道:
单例模式解决的是程序设计层面的实例控制,不是某种跨 JVM、跨服务器的全局唯一性魔法。
五、实践应用
5.1 适合考虑单例的场景
可能包括:
应用统一配置管理
某些全局协调器
日志/任务管理组件
运行时环境管理对象
共享缓存管理器
但必须先分析:
这个业务对象真的应该共享一份状态吗?
5.2 不适合单例的典型对象
例如:
Student
Order
Article
Product
BankAccount
Comment
业务本身就允许多个实体。
如果写成单例:
Student.getInstance()
意味着整个程序只能存在一个 Student 对象。
显然通常不符合业务模型。
5.3 单例与工具类的区别
工具类:
ArrayUtil.max(arr);
通常:
没有业务实例状态
只提供通用静态函数
单例:
ConfigManager
.getInstance()
.getConfig();
仍然存在:
一个真实共享对象
对象中可能保存:
状态
配置
资源引用
生命周期信息
所以二者不是一回事。
六、常见问题
6.1 什么是设计模式?
针对软件开发中重复出现的设计问题,总结出的可复用设计思路或结构方案。
6.2 设计模式是不是唯一最优答案?
不是。
软件设计取决于:
上下文
约束
需求
复杂度
维护成本
一个模式可能在某种环境很好,在另一种环境反而过度设计。
6.3 学设计模式主要学什么?
至少学:
解决什么问题
为什么这样设计
结构是什么
怎么实现
适用场景
优缺点
6.4 什么是单例模式?
控制某个类的实例创建,并向调用者提供一个共享实例的设计模式。
6.5 为什么构造器必须 private?
为了阻止外部随意:
new Singleton();
从根源上控制对象创建入口。
6.6 为什么 instance 通常是 static?
因为在调用者还没有 Singleton 对象的时候,就必须能够通过类本身获取共享实例。
6.7 为什么 getInstance() 是 static?
同样因为调用者无需先创建 Singleton 对象,就可以:
Singleton.getInstance();
6.8 饿汉式什么时候创建对象?
类初始化过程中就创建。
调用 getInstance() 时对象已经存在。
6.9 懒汉式什么时候创建对象?
第一次真正调用获取逻辑并发现实例为空时创建。
6.10 饿汉式与懒汉式最大区别是什么?
对象创建时机。
饿汉:
提前创建
懒汉:
延迟创建
6.11 基础懒汉式线程安全吗?
不能保证。
多个线程可能同时发现:
instance == null
并分别创建对象。
线程安全版本留到并发章节。
6.12 单例就是为了节省内存吗?
不是。
核心是:
控制某个业务概念的实例数量和统一访问。
减少重复实例只可能是其中一个结果。
6.13 Singleton 和 static 工具类一样吗?
不一样。
工具类可以完全没有实例。
单例模式则:
明确存在一个共享实例对象。
6.14 所有 Manager 类都应该单例吗?
不是。
名称叫:
Manager
Service
Config
不能自动推出它应该单例。
必须根据对象职责和生命周期判断。
七、练习与验收
7.1 知识问答
- 什么是设计模式?
- 为什么设计模式不能简单理解成“唯一最优答案”?
- 学习一个设计模式应该关注哪些问题?
- 什么是单例模式?
- 单例模式主要解决什么问题?
- 单例模式核心有哪三个组成部分?
- 为什么构造器要 private?
- 为什么单例实例通常设计成 static?
- 为什么获取方法通常设计成 static?
- 什么是饿汉式单例?
- 什么是懒汉式单例?
- 两者最大的区别是什么?
- 饿汉式实例什么时候创建?
- 懒汉式实例什么时候创建?
- 基础懒汉式为什么存在多线程问题?
- 为什么本章不直接实现双重检查锁等线程安全版本?
- 单例与工具类有什么区别?
- 单例是否只是为了节省内存?
- 单例可能产生哪些设计代价?
- 为什么 Student 通常不应该设计成单例?
7.2 代码阅读
阅读:
public class Singleton {
private static final Singleton INSTANCE =
new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return INSTANCE;
}
}
回答:
- 外部能否
new Singleton()? - 为什么?
INSTANCE属于对象还是类?- 对象什么时候创建?
getInstance()每次返回什么?- 这是饿汉式还是懒汉式?
继续阅读:
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance =
new Singleton();
}
return instance;
}
}
回答:
- 类刚初始化时
instance是什么? - 第一次调用时会发生什么?
- 第二次调用时还会不会
new? - 这属于哪一种单例?
- 多线程环境可能存在什么问题?
7.3 手写代码
任务一:饿汉式
关闭资料,从零编写:
AppConfig
要求:
- 外部不能
new。 - 类初始化时创建实例。
- 提供
getInstance()。 - 连续获取三次。
- 使用
==验证三个引用关系。
任务二:基础懒汉式
从零设计:
CacheManager
要求:
- 构造器私有。
- 开始时不创建对象。
- 第一次
getInstance()时创建。 - 以后始终返回原对象。
- 不使用尚未学习的
synchronized。
任务三:比较创建时机
分别写:
EagerSingleton
LazySingleton
在构造器中打印:
对象创建
观察:
仅加载/使用类
第一次获取实例
第二次获取实例
三个阶段的差别。
7.4 Debug
下面代码能保证单例吗?
public class Singleton {
public Singleton() {
}
private static final Singleton INSTANCE =
new Singleton();
public static Singleton getInstance() {
return INSTANCE;
}
}
回答:
- 最大问题在哪里?
- 外部能否绕开
getInstance()? - 如何修复?
下面代码:
public class Singleton {
private Singleton() {
}
public Singleton getInstance() {
return new Singleton();
}
}
分析:
getInstance()为什么无法在没有对象时方便调用?- 每次调用逻辑又为什么违背单例目标?
- 正确设计需要哪些 static 成员?
下面代码:
if (instance == null) {
instance = new Singleton();
}
假设线程 A、B 同时运行。
只描述:
- 两个线程可能分别看到什么?
- 最坏情况下创建几个对象?
- 为什么会破坏单例?
- 暂时不要实现线程安全版本。
7.5 综合训练
设计:
ApplicationSettings
保存:
siteName
pageSize
debugMode
要求:
- 应用程序只通过一个共享设置对象读取配置。
- 使用饿汉式单例。
- 禁止外部创建实例。
- 使用
getInstance()获取。 - 编写两个不同业务类,分别获取 ApplicationSettings。
- 验证两个业务类获得的是同一个对象。
- 分析如果 ApplicationSettings 内部允许大量代码随意修改状态,会产生什么设计风险。
然后回答:
单例真正解决的是“少创建几个对象”,还是“控制一个共享业务概念的实例与访问入口”?为什么?
7.6 本章验收
关闭资料和 AI 自动补全,确认自己能够:
- [ ] 解释什么是设计模式。
- [ ] 说出学习设计模式的核心维度。
- [ ] 解释单例模式解决什么问题。
- [ ] 从零写出 private constructor。
- [ ] 从零写出饿汉式单例。
- [ ] 从零写出基础懒汉式单例。
- [ ] 使用
==验证获取的是同一对象。 - [ ] 解释饿汉与懒汉的初始化时机。
- [ ] 解释基础懒汉式的线程风险。
- [ ] 区分单例和 static 工具类。
- [ ] 判断某个业务类到底应不应该单例。
- [ ] 解释单例的优点与潜在代价。
如果你只能背:
private 构造器
static 对象
static getInstance()
却无法解释:
为什么这个业务类需要只有一个共享实例?
为什么构造器必须被控制?
为什么基础懒汉式多线程下会出问题?
那么还没有真正掌握单例模式。