【HF设计模式】05-单例模式
声明:仅为个人学习总结,还请批判性查看,如有不同观点,欢迎交流。
摘要
《Head First设计模式》第5章笔记:结合示例应用和代码,介绍单例模式,包括遇到的问题、采用的解决方案、以及达到的效果。
目录
- 摘要
- 1 示例应用
- 2 引入设计模式
- 2.1 私有构造方法
- 2.2 静态方法
- 2.3 静态变量
- 2.4 经典单例实现
- 2.5 单例模式定义
- 2.6 第1版改进
- 3 遇到问题
- 3.1 多线程方案1:同步方法
- 3.2 多线程方案2:急切实例化
- 3.3 多线程方案3:双重检查锁定
- 3.4 其它注意事项
- 3.5 使用枚举
- 4 示例代码
- 4.1 Java 示例
- 4.2 C++11 示例
- 5 设计工具箱
- 5.1 OO 基础
- 5.2 OO 原则
- 5.3 OO 模式
- 参考
1 示例应用
示例应用是巧克力工厂的锅炉控制系统。巧克力锅炉将巧克力和牛奶混合,加热至沸腾,然后将它们送到制作巧克力棒的下一阶段。
锅炉的状态包括 boolean empty
(是否为空)和 boolean boiled
(是否沸腾),相应的状态转换情况如下:
下面是巧克力锅炉的定义,在执行 fill()
、boil()
、drain()
操作时,都进行了严格的状态判断,防止出现锅炉空烧、排出未煮沸巧克力等糟糕情况。
public class ChocolateBoiler {
private boolean empty;
private boolean boiled;
public ChocolateBoiler() {
empty = true;
boiled = false;
}
public void fill() {
if (isEmpty()) { // 状态为空时,才可以执行操作
System.out.println("向锅炉中加满牛奶和巧克力");
empty = false;
boiled = false;
}
}
public void drain() {
if (!isEmpty() && isBoiled()) { // 状态为满并且沸腾时,才可以执行操作
System.out.println("从锅炉中排出牛奶和巧克力");
empty = true;
boiled = false;
}
}
public void boil() {
if (!isEmpty() && !isBoiled()) { // 状态为满并且未沸腾时,才可以执行操作
System.out.println("将锅炉中的牛奶和巧克力煮沸");
boiled = true;
}
}
public boolean isEmpty() { return empty; }
public boolean isBoiled() { return boiled; }
}
除了状态监控,为保证系统正常运行,还要避免为一台锅炉创建多个 ChocolateBoiler
实例。否则,由它们共同操作锅炉,情况也会很糟糕。
2 引入设计模式
接下来,我们尝试通过设计模式来确保一个类 ChocolateBoiler
只能创建单一实例(单例)。
显然,通过 new ChocolateBoiler()
可以创建一个实例;但是,再次执行 new ChocolateBoiler()
还会创建另一个实例。
所以,我们的目标是让 new ChocolateBoiler()
只能被执行一次。
要达成目标,可以分为下面 3 个步骤。
2.1 私有构造方法
以 MyClass
类为例。
思考题
下面哪个或哪些选项可以将 MyClass 类实例化?【答案在第 20 行】
public class MyClass {
// ...
private MyClass() {}
}
A. MyClass 的包外类
B. MyClass 的同包类
C. MyClass 的子类
D. MyClass 的内部代码
E. 没有任何办法
答案:D
解析:一个类的 private 成员(包括构造方法)只能从“它所在类的内部”访问。
达成目标的第1步:定义私有构造方法,阻止外部类直接执行 new MyClass()
创建实例。
2.2 静态方法
类的静态成员(静态方法和静态变量),属于类本身,而不属于类的某个实例。
通过在类的内部定义 getInstance()
方法,可以访问类的私有构造方法,创建实例。
public class MyClass {
// ...
private MyClass() {}
public static MyClass getInstance() {
return new MyClass();
}
}
通过将 getInstance()
声明为 static
静态方法,能够在不创建任何实例的情况下,直接使用类名访问 getInstance()
。
MyClass obj = MyClass.getInstance();
达成目标的第2步:定义静态方法,由类自身执行 new MyClass()
创建实例,并对外提供获取实例的统一接口。
2.3 静态变量
静态变量属于类本身,用于存储“类级别”的状态或共享数据。由于它与类的任何实例都无关,所以可以用来控制实例的创建。
通过在类的内部定义静态变量 uniqueInstance
,控制 MyClass
只能创建单一实例(单例)。
public class MyClass {
private static MyClass uniqueInstance; // 在类加载时被初始化为 null
private MyClass() {}
public static MyClass getInstance() {
if (uniqueInstance == null) { // 在还没有创建任何实例的情况下,可以创建实例,确保实例的唯一性
uniqueInstance = new MyClass(); // 使用 uniqueInstance 引用该实例
}
return uniqueInstance; // 返回已经创建的实例
}
}
达成目标的第3步:定义静态变量,引用类的唯一实例,限制 new MyClass()
只能被执行一次。
2.4 经典单例实现
我们使用一个私有构造方法、一个静态方法、一个静态变量,实现了经典的单例模式。
public class Singleton {
// 一个静态变量(私有),引用 Singleton 的唯一实例
private static Singleton uniqueInstance;
// 在这里添加其它有用的变量
// 将构造方法声明为私有的,只有 Singleton 可以实例化这个类
private Singleton() {
// ...
}
// 一个静态方法(公共),用于创建唯一的 Singleton 实例,并将其返回
public static Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
// 在这里添加其它有用的方法
// 当然,Singleton 是一个正常的类,会有一些实现相应功能的变量和方法
}
在单例模式中:
- 由一个类来管理自己的唯一实例,要想访问实例,只能通过该类;
- 在需要访问实例时,只需要调用该类提供的静态方法(即,该实例的全局访问点)。
2.5 单例模式定义
单例模式(Singleton Pattern)
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
Ensure a class only has one instance, and provide a global point of access to it.
- 私有(或保护)构造函数
Singleton()
,确保外部无法直接实例化; - 私有(或保护)静态成员变量
uniqueInstance
,引用Singleton
的唯一实例; - 公共静态成员方法
getInstance()
,提供访问Singleton
唯一实例的全局接口; - 业务相关的数据
usefulSingletonData
和方法usefulSingletonMethod()
,用于实现类的具体功能。
单例模式的优点:
- 控制实例访问
通过getInstance()
方法,可以严格控制实例的访问时机和访问方式。 - 相比全局变量的优势
- 实例唯一性:单例可以保证实例唯一,而全局变量无法保证;
- 延迟初始化:单例可以根据需要创建实例,而全局变量在程序启动时就会创建(无论是否会用到);
- 命名空间占用:单例的实例被封装在类内部,而全局变量直接占用全局命名空间(命名空间污染)。
- 实例数量可变
通过修改 2.3 静态变量 的实现,也可以让Singleton
类管理自己的多个实例。
延伸阅读:《设计模式:可复用面向对象软件的基础》 3.5 Singleton(单件)— 对象创建型模式 [P96-102]
2.6 第1版改进
采用单例模式,已经实现让一个类只能创建单一实例。下面是改进后的 ChocolateBoiler
类:
public class ChocolateBoiler {
private boolean empty;
private boolean boiled;
// 增加静态变量、静态方法,修改构造方法为私有
private static ChocolateBoiler uniqueInstance;
private ChocolateBoiler() {
empty = true;
boiled = false;
}
public static ChocolateBoiler getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new ChocolateBoiler();
}
return uniqueInstance;
}
// 后面的代码没有变化
public void fill() {
if (isEmpty()) { // 状态为空时,才可以执行操作
System.out.println("向锅炉中加满牛奶和巧克力");
empty = false;
boiled = false;
}
}
public void drain() {
if (!isEmpty() && isBoiled()) { // 状态为满并且沸腾时,才可以执行操作
System.out.println("从锅炉中排出牛奶和巧克力");
empty = true;
boiled = false;
}
}
public void boil() {
if (!isEmpty() && !isBoiled()) { // 状态为满并且未沸腾时,才可以执行操作
System.out.println("将锅炉中的牛奶和巧克力煮沸");
boiled = true;
}
}
public boolean isEmpty() { return empty; }
public boolean isBoiled() { return boiled; }
}
3 遇到问题
糟糕!在使用多线程对巧克力锅炉控制器进行优化后,锅炉发生了溢出!
我们来查找一下问题的原因。
下面是锅炉控制器 BoilerController
中的相关代码:
ChocolateBoiler boiler = ChocolateBoiler.getInstance();
boiler.fill(); // 加满
boiler.boil(); // 煮沸
boiler.drain(); // 排出
现在有两个线程都需要执行上述代码:
- 如果它们引用相同的
ChocolateBoiler
实例,就会共享一致的状态,并通过对状态的严格监控,让锅炉运转良好;(此处忽略数据竞争) - 如果它们引用不同的
ChocolateBoiler
实例,就会拥有各自的状态,如果一个实例已经加满锅炉,而另一个实例还是空置状态,并继续加入原料,就会导致锅炉溢出。
可是,ChocolateBoiler
已经定义为单例,这两个线程还会引用不同的实例吗?
我们来仔细分析一下 getInstance()
的定义,以及它可能的执行时序。
public static ChocolateBoiler getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new ChocolateBoiler();
}
return uniqueInstance;
}
时序 | 线程1 | 线程2 | uniqueInstance 值 |
---|---|---|---|
1 | ChocolateBoiler.getInstance() | null | |
2 | if (uniqueInstance == null) | null | |
3 | 挂起 | ChocolateBoiler.getInstance() | null |
4 | if (uniqueInstance == null) | null | |
5 | uniqueInstance = new ChocolateBoiler(); | object1 | |
6 | return uniqueInstance; | object1 | |
7 | uniqueInstance = new ChocolateBoiler(); | object2 | |
8 | return uniqueInstance; | object2 |
在多线程环境下,果然有可能创建两个 ChocolateBoiler
实例 object1
和 object2
。
为了确保实例的创建是线程安全的,我们有多种可选方案,包括同步方法、急切实例化、双重检查锁定等。
3.1 多线程方案1:同步方法
只要把 getInstance()
变成同步方法(添加 synchronized
关键字),就可以保证实例创建的线程安全性。
Java 概念:内在锁(intrinsic lock)
每个 Java 对象都有一个内在锁,获得对象的内在锁就能够独占该对象的访问权,试图访问被锁定对象的线程将被阻塞,直到持有该锁的线程释放锁。使用synchronized
关键字可以获得对象的内在锁。
Java 概念:方法同步(Method Synchronization)
- 当一个线程调用一个对象的非静态
synchronized
方法时,它会在方法执行之前,自动尝试获得该对象的内在锁;在方法返回之前,线程一直持有锁。- 一旦某个线程锁定了某个对象,其他线程就不能执行同一个对象的“同一方法或其他同步方法”,只能阻塞等待,直到这个锁再次变成可用的为止。
- 锁还可以重入(reentrant),这意味着持有锁的线程可以调用同一对象上的其他同步方法;当最外层同步方法返回时,会释放该对象的内在锁。
- 静态方法也可以同步,在这种情况下,会使用与该方法的类关联的
Class
对象的锁(每个类都有一个对应的Class
对象,如Singleton.class
,包含该类的元数据)。
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {}
// synchronized 保证没有两个线程可以同时执行 getInstance()
// 一旦某个线程开始执行 getInstance(),就会获得锁;其它线程再调用 getInstance(),会阻塞等待,直到持有锁的线程释放锁
public static synchronized Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
public String getDescription() { return "I'm a thread safe Singleton!"; }
}
同步方法实现简单,但是也有明显的缺点:
- 运行时开销大:同步一个方法可能会使性能下降 100 倍(synchronizing a method can decrease performance by a factor of 100);
- 存在不必要的资源浪费:实际上,只有第一次执行
getInstance()
创建实例时,才需要同步;然而,现在每次执行getInstance()
都需要同步。
不过,如果 getInstance()
的性能对应用来说并不重要,那么使用同步方法也没有问题。
3.2 多线程方案2:急切实例化
在调用 getInstance()
时创建实例,被称为延迟初始化(Lazy Initialization);
与之相对的,可以在类加载时直接创建实例,即急切初始化(Eager Initialization);因为类加载具有线程安全性,所以实例的创建也是线程安全的。
public class Singleton {
// 在类加载时直接创建实例,初始化静态变量
private static Singleton uniqueInstance = new Singleton();
private Singleton() {}
// 直接返回已经创建的实例
public static Singleton getInstance() {
return uniqueInstance;
}
public String getDescription() { return "I'm a thread safe Singleton!"; }
}
急切实例化的特点和相关影响如下:
- 特点:一定会创建实例;
影响:如果应用有可能不使用实例,那么会造成不必要的资源浪费。 - 特点:在应用启动时就会创建实例;
影响:如果实例的创建或运行比较消耗资源,那么会给应用带来一定的负担。
如果应用需要尽早的使用实例,或者即便使用的时间比较晚,但实例的创建和运行负担并不重,那么也可以选择急切实例化。
3.3 多线程方案3:双重检查锁定
双重检查锁定(Double-Checked Locking):先检查实例是否已经创建,如果尚未创建,才进行同步。
以此减少 同步方法 中 getInstance()
对同步的使用。(需要 Java 5 及以后版本)
Java 概念:块同步(Block Synchronization)
Java 允许使用synchronized
关键字来锁定任何对象,从而实现代码块的同步。
synchronized(object) { // 在 object 被锁定的情况下执行某些操作 }
在块中的代码执行之后,锁会被释放。
Java 概念:可见性(Visibility)
在一个单线程程序中,读取变量的值总是会得到最后写入该变量的值。但是,在 Java 的多线程应用程序中,一个线程可能看不到另一个线程所做的更改,除非在数据上执行的操作是同步的。然而,同步是有代价的。如果想要的是可见性,而不需要互斥,那么可以使用volatile
关键字,该关键字可以确保当一个线程修改了变量的值之后,新值对于其他线程立即可见。
Java 概念:指令重排序(Instruction Reordering)
编译器或处理器为了优化程序性能,可能会改变指令的执行顺序。
例如对于uniqueInstance = new Singleton();
语句,其包含的步骤示意如下:
memory = allocate(sizeof(Singleton.class));
在堆内存中为对象分配空间construct(memory, Singleton.class);
在分配的内存空间上调用构造函数来初始化对象uniqueInstance = memory;
将对象引用指向分配的内存空间在进行指令重排序后,步骤3可能会在步骤2之前执行。如果另一个线程在“步骤3之后、步骤2之前”访问
uniqueInstance
,就会获取到一个未完全初始化的实例。通过将uniqueInstance
声明为volatile
,可以禁止这种重排序。这样,当uniqueInstance
不为null
时,它所引用的实例就是完全初始化的。
public class Singleton {
// 将 uniqueInstance 声明为 volatile,确保其可见性,并禁止指令重排序
private volatile static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getInstance() {
if (uniqueInstance == null) { // 【第一次检查】实例是否创建,只有尚未创建,才进入同步块;
synchronized (Singleton.class) { // 尝试加锁,如果其它线程已经加锁,则阻塞等待;
if (uniqueInstance == null) { // 成功加锁后,【再次检查】实例是否尚未创建,
uniqueInstance = new Singleton(); // 因为在阻塞等待的过程中,其它线程可能已经创建实例。
}
}
}
return uniqueInstance;
}
public String getDescription() { return "I'm a thread safe Singleton!"; }
}
如果使用同步方法存在性能问题,那么使用双重检查锁定,则可以兼顾性能和线程安全。
3.4 其它注意事项
除了创建实例时的线程安全问题,在 java 中,使用单例还有一些其它注意事项:
- 类加载器问题
- 问题描述:如果有两个或多个类加载器,就可以多次加载同一个类(每个类加载器一次)。如果这个类刚好是一个单例,就会有多于一个的实例。
- 解决办法:确保单例通过同一个类加载器加载,通常使用系统类加载器(即启动类加载器)加载单例。
- 反射问题
- 问题描述:通过反射可以调用类的私有构造方法,因此可能会创建类的多个实例。
- 解决办法:在构造方法中添加防御性代码,防止通过反射创建多个实例。例如,可以在构造方法中检查是否已经存在实例,如果存在则抛出异常。
- 序列化和反序列化问题
- 问题描述:当单例实现了
Serializable
接口时,序列化会将对象的状态保存下来,之后反序列化可以重建对象,这样可能会创建类的多个实例。 - 解决办法:在单例中添加一个
readResolve
方法,该方法在反序列化时会被调用,可以返回单例实例,而不是创建一个新的实例。
- 问题描述:当单例实现了
3.5 使用枚举
Java 概念:枚举(Enum)
在 Java 中,枚举是一种特殊的类,是java.lang.Enum
的子类,它的特性包括:
- 枚举类默认具有私有的构造方法,而且不允许显式定义非私有构造方法,因此无法从外部实例化;并且也不能通过反射来访问构造方法;
- 枚举实例在类被加载到 JVM 时静态初始化,保证了实例的唯一性和线程安全性;
- 枚举类在序列化和反序列化的过程中,会由 JVM 保证枚举实例的唯一性;
- 每个枚举常量自动被视为
public static final
,并且是枚举类型的一个实例;- 枚举类也可以定义自己的方法和变量。
因为 Java 会保证枚举类中每个枚举常量的唯一性,所以通过定义一个包含单个枚举常量的枚举类,就可以自然地实现单例模式。
public enum Singleton {
UNIQUE_INSTANCE;
// 可以添加有用的变量和方法
public String getDescription() {
return "I'm a thread safe Singleton!";
}
}
枚举的使用:
Singleton singleton = Singleton.UNIQUE_INSTANCE;
System.out.println(singleton.getDescription());
使用枚举实现单例,代码简洁明了,而且可以避免前文提到的所有单例问题,包括创建实例时的线程安全、类加载问题、反射问题、以及序列化和反序列化问题。因此,枚举是实现单例模式的一种推荐方式。
4 示例代码
4.1 Java 示例
双重检查锁定方式:
// ChocolateBoiler.java
public class ChocolateBoiler {
private boolean empty;
private boolean boiled;
private volatile static ChocolateBoiler uniqueInstance;
private ChocolateBoiler() {
empty = true;
boiled = false;
System.out.println("[" + Thread.currentThread().getName() + "] 创建巧克力锅炉实例,初始状态:空、未沸");
}
public static ChocolateBoiler getInstance() {
if (uniqueInstance == null) {
synchronized (ChocolateBoiler.class) {
if (uniqueInstance == null) {
uniqueInstance = new ChocolateBoiler();
}
}
}
System.out.println("[" + Thread.currentThread().getName() + "] 返回巧克力锅炉实例,当前状态:" + (uniqueInstance.isEmpty() ? "空" : "满") + "、" + (uniqueInstance.isBoiled() ? "沸腾" : "未沸"));
return uniqueInstance;
}
public synchronized void fill() {
System.out.println("[" + Thread.currentThread().getName() + "] 尝试加满,当前状态:" + (isEmpty() ? "空" : "满") + "、" + (isBoiled() ? "沸腾" : "未沸"));
if (isEmpty()) {
System.out.println(" => 向锅炉中加满牛奶和巧克力");
empty = false;
boiled = false;
}
}
public synchronized void drain() {
System.out.println("[" + Thread.currentThread().getName() + "] 尝试排出,当前状态:" + (isEmpty() ? "空" : "满") + "、" + (isBoiled() ? "沸腾" : "未沸"));
if (!isEmpty() && isBoiled()) {
System.out.println(" => 从锅炉中排出牛奶和巧克力");
empty = true;
boiled = false;
}
}
public synchronized void boil() {
System.out.println("[" + Thread.currentThread().getName() + "] 尝试煮沸,当前状态:" + (isEmpty() ? "空" : "满") + "、" + (isBoiled() ? "沸腾" : "未沸"));
if (!isEmpty() && !isBoiled()) {
System.out.println(" => 将锅炉中的牛奶和巧克力煮沸");
boiled = true;
}
}
public synchronized boolean isEmpty() { return empty; }
public synchronized boolean isBoiled() { return boiled; }
}
枚举方式:
// ChocolateBoilerEnum.java
public enum ChocolateBoilerEnum {
UNIQUE_INSTANCE;
private boolean empty;
private boolean boiled;
private ChocolateBoilerEnum() {
empty = true;
boiled = false;
System.out.println("[" + Thread.currentThread().getName() + "] 创建巧克力锅炉实例,初始状态:空、未沸");
}
public synchronized void fill() { /* 代码相同 */ }
public synchronized void drain() { /* 代码相同 */ }
public synchronized void boil() { /* 代码相同 */ }
public synchronized boolean isEmpty() { /* 代码相同 */ }
public synchronized boolean isBoiled() { /* 代码相同 */ }
}
测试代码:
// BoilerController.java
public class BoilerController {
public static void main(String args[]) {
Runnable boilerTask = new Runnable() {
@Override
public void run() {
ChocolateBoiler boiler = ChocolateBoiler.getInstance();
// ChocolateBoilerEnum boiler = ChocolateBoilerEnum.UNIQUE_INSTANCE;
boiler.fill(); // 加满
boiler.boil(); // 煮沸
boiler.drain(); // 排出
}
};
Thread thread1 = new Thread(boilerTask);
Thread thread2 = new Thread(boilerTask);
thread1.start();
thread2.start();
}
}
4.2 C++11 示例
4.2.1 Meyers Singleton
Meyers Singleton 是一种在 C++ 中实现单例模式的简洁方法,由 Scott Meyers(Effective C++ 的作者)提出。
struct Singleton {
static Singleton& getInstance() {
static Singleton uniqueInstance;
return uniqueInstance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
};
这种方法基于 C++11 的保证,即局部静态对象会在“函数第一次执行到该对象的定义处”时被初始化(同时具有线程安全性)。作为额外的好处,如果从未调用“模拟非局部静态对象”的函数(即
getInstance()
函数,使用在函数内部定义的“局部静态对象”,模拟在全局/命名空间范围内定义的“非局部静态对象”),就永远不会产生构造和析构该对象的开销。
Scott Myers says: “This approach is founded on C++'s guarantee that local static objects are initialized when the object’s definition is first encountered during a call to that function.” … “As a bonus, if you never call a function emulating a non-local static object, you never incur the cost of constructing and destructing the object.”
4.2.2 ChocolateBoiler
局部静态变量方式(Meyers Singleton):
struct ChocolateBoiler {
static ChocolateBoiler& getInstance() {
// 对于局部静态变量,由 C++11 保证只在第一次执行到变量定义处时,进行初始化,并且是线程安全的
static ChocolateBoiler uniqueInstance;
std::cout << "[" << std::this_thread::get_id() << "] 返回巧克力锅炉实例,当前状态:" << (uniqueInstance.isEmpty() ? "空" : "满") << "、" << (uniqueInstance.isBoiled() ? "沸腾" : "未沸") << '\n';
return uniqueInstance;
}
void fill() {
std::lock_guard<std::recursive_mutex> guard(mtx);
std::cout << "[" << std::this_thread::get_id() << "] 尝试加满,当前状态:" << (isEmpty() ? "空" : "满") << "、" << (isBoiled() ? "沸腾" : "未沸") << '\n';
if (isEmpty()) {
std::cout << " => 向锅炉中加满牛奶和巧克力\n";
empty = false;
boiled = false;
}
}
void drain() {
std::lock_guard<std::recursive_mutex> guard(mtx);
std::cout << "[" << std::this_thread::get_id() << "] 尝试排出,当前状态:" << (isEmpty() ? "空" : "满") << "、" << (isBoiled() ? "沸腾" : "未沸") << '\n';
if (!isEmpty() && isBoiled()) {
std::cout << " => 从锅炉中排出牛奶和巧克力\n";
empty = true;
boiled = false;
}
}
void boil() {
std::lock_guard<std::recursive_mutex> guard(mtx);
std::cout << "[" << std::this_thread::get_id() << "] 尝试煮沸,当前状态:" << (isEmpty() ? "空" : "满") << "、" << (isBoiled() ? "沸腾" : "未沸") << '\n';
if (!isEmpty() && !isBoiled()) {
std::cout << " => 将锅炉中的牛奶和巧克力煮沸\n";
boiled = true;
}
}
bool isEmpty() const {
std::lock_guard<std::recursive_mutex> guard(mtx);
return empty;
}
bool isBoiled() const {
std::lock_guard<std::recursive_mutex> guard(mtx);
return boiled;
}
ChocolateBoiler(const ChocolateBoiler&) = delete;
ChocolateBoiler& operator=(const ChocolateBoiler&) = delete;
private:
ChocolateBoiler() : empty(true), boiled(false) {
std::cout << "[" << std::this_thread::get_id() << "] 创建巧克力锅炉实例,初始状态:空、未沸\n";
}
bool empty;
bool boiled;
static std::recursive_mutex mtx;
};
std::recursive_mutex ChocolateBoiler::mtx;
测试代码:
#include <iostream>
#include <mutex>
#include <thread>
// 在这里添加相关接口和类的定义
int main() {
std::function<void()> boilerTask = []() {
ChocolateBoiler& boiler = ChocolateBoiler::getInstance();
boiler.fill();
boiler.boil();
boiler.drain();
};
std::thread t1(boilerTask);
std::thread t2(boilerTask);
t1.join();
t2.join();
}
4.2.3 一次性互斥
与 Java 的双重检查锁定相比,C++11 提供了更为简洁的“一次性互斥”机制。
通过 std::once_flag
类和 std::call_once()
函数模板来实现一次性互斥,确保即使有多个线程、多次、同时调用某个函数(可调用对象),其只会被执行一次。复习回顾 线程池2-线程互斥 => 3.1.3 一次性互斥。
struct Singleton {
static Singleton& getInstance() {
std::call_once(initFlag, []() { uniqueInstance.reset(new Singleton()); });
return *uniqueInstance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
static std::unique_ptr<Singleton> uniqueInstance;
static std::once_flag initFlag;
};
std::unique_ptr<Singleton> Singleton::uniqueInstance;
std::once_flag Singleton::initFlag;
5 设计工具箱
5.1 OO 基础
OO 基础回顾
- 抽象(Abstraction)
- 封装(Encapsulation)
- 继承(Inheritance)
- 多态(Polymorphism)
5.2 OO 原则
5.2.1 新原则
本章没有介绍新的 OO 原则。
5.2.2 原则回顾
- 封装变化。
Encapsulate what varies. - 针对接口编程,而不是针对实现编程。
Program to interfaces, not implementations. - 优先使用组合,而不是继承。
Favor composition over inheritance. - 尽量做到交互对象之间的松耦合设计。
Strive for loosely coupled designs between objects that interact. - 类应该对扩展开放,对修改关闭。
Classes should be open for extension, but closed for modification. - 依赖抽象,不依赖具体类。
Depend on abstractions. Do not depend on concrete classes.
5.3 OO 模式
5.3.1 新模式
单例模式(Singleton Pattern)
- 确保一个类只有一个实例,并提供一个全局访问点。
The Singleton Pattern ensures a class has only one instance, and provides a global point of access to it.
5.3.2 模式回顾
1 创建型模式(Creational Patterns)
创建型模式与对象的创建有关。
Creational patterns concern the process of object creation.
- 工厂方法(Factory Method)
- 定义了一个创建对象的接口,但由子类决定要实例化哪个类。
The Factory Method Pattern defines an interface for creating an object, but lets subclasses decide which class to instantiate. - 工厂方法让类把实例化推迟到子类。
Factory Method lets a class defer instantiation to subclasses.
- 定义了一个创建对象的接口,但由子类决定要实例化哪个类。
- 抽象工厂(Abstract Factory)
- 提供一个接口,创建相关或依赖对象的家族,而不需要指定具体类。
The Abstract Factory Pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes.
- 提供一个接口,创建相关或依赖对象的家族,而不需要指定具体类。
2 结构型模式(Structural Patterns)
结构型模式处理类或对象的组合。
Structural patterns deal with the composition of classes or objects.
- 装饰者模式(Decorator Pattern)
- 动态地给一个对象添加一些额外的职责。
The Decorator Pattern attaches additional responsibilities to an object dynamically. - 就增加功能来说,装饰者模式相比生成子类更为灵活。
Decorators provide a flexible alternative to subclassing for extending functionality.
- 动态地给一个对象添加一些额外的职责。
3 行为型模式(Behavioral Patterns)
行为型模式描述类或对象之间的交互方式以及职责分配方式。
Behavioral patterns characterize the ways in which classes or objects interact and distribute responsibility.
- 策略模式(Strategy Pattern)
- 定义一个算法家族,把其中的算法分别封装起来,使得它们之间可以互相替换。
Strategy defines a family of algorithms, encapsulates each one, and makes them interchangeable. - 让算法的变化独立于使用算法的客户。
Strategy lets the algorithm vary independently from clients that use it.
- 定义一个算法家族,把其中的算法分别封装起来,使得它们之间可以互相替换。
- 观察者模式(Observer Pattern)
- 定义对象之间的一对多依赖,
The Observer Pattern defines a one-to-many dependency between objects - 这样一来,当一个对象改变状态时,它的所有依赖者都会被通知并自动更新。
so that when one object changes state, all of its dependents are notified and updated automatically.
- 定义对象之间的一对多依赖,
参考
- [美]弗里曼、罗布森著,UMLChina译.Head First设计模式.中国电力出版社.2022.2
- [美]伽玛等著,李英军等译.设计模式:可复用面向对象软件的基础.机械工业出版社.2019.3
- wickedlysmart: Head First设计模式 Java 源码
- [加]布迪·克尼亚万著,沈泽刚译.Java经典入门指南.人民邮电出版社.2020.6
Hi, I’m the ENDing, nice to meet you here! Hope this article has been helpful.