设计模式入门与单例模式 | 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 知识问答

  1. 什么是设计模式?
  2. 为什么设计模式不能简单理解成“唯一最优答案”?
  3. 学习一个设计模式应该关注哪些问题?
  4. 什么是单例模式?
  5. 单例模式主要解决什么问题?
  6. 单例模式核心有哪三个组成部分?
  7. 为什么构造器要 private?
  8. 为什么单例实例通常设计成 static?
  9. 为什么获取方法通常设计成 static?
  10. 什么是饿汉式单例?
  11. 什么是懒汉式单例?
  12. 两者最大的区别是什么?
  13. 饿汉式实例什么时候创建?
  14. 懒汉式实例什么时候创建?
  15. 基础懒汉式为什么存在多线程问题?
  16. 为什么本章不直接实现双重检查锁等线程安全版本?
  17. 单例与工具类有什么区别?
  18. 单例是否只是为了节省内存?
  19. 单例可能产生哪些设计代价?
  20. 为什么 Student 通常不应该设计成单例?

7.2 代码阅读

阅读:

public class Singleton {

    private static final Singleton INSTANCE =
            new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

回答:

  1. 外部能否 new Singleton()
  2. 为什么?
  3. INSTANCE 属于对象还是类?
  4. 对象什么时候创建?
  5. getInstance() 每次返回什么?
  6. 这是饿汉式还是懒汉式?

继续阅读:

public class Singleton {

    private static Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {

        if (instance == null) {
            instance =
                    new Singleton();
        }

        return instance;
    }
}

回答:

  1. 类刚初始化时 instance 是什么?
  2. 第一次调用时会发生什么?
  3. 第二次调用时还会不会 new
  4. 这属于哪一种单例?
  5. 多线程环境可能存在什么问题?

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;
    }
}

回答:

  1. 最大问题在哪里?
  2. 外部能否绕开 getInstance()
  3. 如何修复?

下面代码:

public class Singleton {

    private Singleton() {
    }

    public Singleton getInstance() {
        return new Singleton();
    }
}

分析:

  1. getInstance() 为什么无法在没有对象时方便调用?
  2. 每次调用逻辑又为什么违背单例目标?
  3. 正确设计需要哪些 static 成员?

下面代码:

if (instance == null) {
    instance = new Singleton();
}

假设线程 A、B 同时运行。

只描述:

  1. 两个线程可能分别看到什么?
  2. 最坏情况下创建几个对象?
  3. 为什么会破坏单例?
  4. 暂时不要实现线程安全版本。

7.5 综合训练

设计:

ApplicationSettings

保存:

siteName
pageSize
debugMode

要求:

  1. 应用程序只通过一个共享设置对象读取配置。
  2. 使用饿汉式单例。
  3. 禁止外部创建实例。
  4. 使用 getInstance() 获取。
  5. 编写两个不同业务类,分别获取 ApplicationSettings。
  6. 验证两个业务类获得的是同一个对象。
  7. 分析如果 ApplicationSettings 内部允许大量代码随意修改状态,会产生什么设计风险。

然后回答:

单例真正解决的是“少创建几个对象”,还是“控制一个共享业务概念的实例与访问入口”?为什么?

7.6 本章验收

关闭资料和 AI 自动补全,确认自己能够:

  • [ ] 解释什么是设计模式。
  • [ ] 说出学习设计模式的核心维度。
  • [ ] 解释单例模式解决什么问题。
  • [ ] 从零写出 private constructor。
  • [ ] 从零写出饿汉式单例。
  • [ ] 从零写出基础懒汉式单例。
  • [ ] 使用 == 验证获取的是同一对象。
  • [ ] 解释饿汉与懒汉的初始化时机。
  • [ ] 解释基础懒汉式的线程风险。
  • [ ] 区分单例和 static 工具类。
  • [ ] 判断某个业务类到底应不应该单例。
  • [ ] 解释单例的优点与潜在代价。

如果你只能背:

private 构造器
static 对象
static getInstance()

却无法解释:

为什么这个业务类需要只有一个共享实例?
为什么构造器必须被控制?
为什么基础懒汉式多线程下会出问题?

那么还没有真正掌握单例模式。