【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; // 在类加载时被初始化为 nullprivate 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() {@Overridepublic 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.
相关文章:

【HF设计模式】05-单例模式
声明:仅为个人学习总结,还请批判性查看,如有不同观点,欢迎交流。 摘要 《Head First设计模式》第5章笔记:结合示例应用和代码,介绍单例模式,包括遇到的问题、采用的解决方案、以及达到的效果。…...
运维人员的Python详细学习路线
以下是一条适合运维人员的Python详细学习路线: 一、基础入门阶段(第1 - 2个月) 环境搭建与基础语法(第1个月) 安装与配置 在运维常用的操作系统(如Linux或Windows)上安装Python。对于Linux系统…...
软件体系结构与设计模式
在软件开发中,软件体系结构和设计模式是两个至关重要的概念。它们帮助开发者设计出易于理解、可扩展、可维护的系统。尽管这两个概念密切相关,但它们分别关注系统的不同方面:软件体系结构关注的是系统整体结构的设计,而设计模式则…...

安徽省地图arcgis数据美化后mxd文件shp格式下载后内容测评
标题中的“安徽省地图arcgis数据美化后mxd文件shp格式”揭示了这个压缩包的内容是经过GIS处理的、针对安徽省地图数据。ArcGIS是一款由Esri公司开发的专业地理信息系统软件,用于处理、分析和展示地理空间数据。MXD文件是ArcGIS的项目文件,包含了地图布局…...
MySQL数据库备份与恢复策略
数据是企业和应用的核心资产,可靠的备份和恢复策略是确保数据安全性和业务连续性的关键。在本篇文章中,我们将详细介绍 MySQL 数据库的备份和恢复方法,包括逻辑备份、物理备份、自动化备份,以及常见问题的处理方法。 一、逻辑备份 逻辑备份是通过导出数据库的结构和数据生…...

go语言zero框架中教务crm系统的在职继承和离职交接的设计与实践
在GoZero中实现一个在职继承和离职交接的通用模块,涉及到顾问离职交接客户、领导离职交接审批单据等功能。为了使这个模块通用且易于扩展,我们可以分成几个部分: 1. **数据模型设计**:我们首先需要设计离职交接相关的数据模型。 …...
C# 设计模式(结构型模式):桥接模式
C# 设计模式(结构型模式):桥接模式 在软件设计中,我们经常会遇到系统的变化频繁,或者需要灵活扩展功能的场景。这时,桥接模式(Bridge Pattern)便显得尤为重要。桥接模式是一个结构型…...
C# 设计模式(行为型模式):解释器模式
C# 设计模式(行为型模式):解释器模式 (Interpreter Pattern) 什么是解释器模式? 解释器模式(Interpreter Pattern)是一种行为型设计模式,用于定义一种语言的语法表示,并提供一个解释…...
如何 cURL Elasticsearch:进入 Shell
作者:来自 Elastic Philipp Krenn Kibana 的控制台是开始使用 Elasticsearch 的 REST API 的最简单方法 - 语法突出显示、自动完成、格式化、导出 cURL、JavaScript 或 Python。而且你不必担心正确的端点、身份验证等。但是有时,如果 Kibana 不可用、你…...

深信服云桌面系统的终端安全准入设置
深信服的云桌面系统在默认状态下没有终端的安全准入设置,这也意味着同样的虚拟机,使用云桌面终端或者桌面套件都可以登录,但这也给系统带来了一些安全隐患,所以,一般情况下需要设置终端的安全准入策略,防止…...
Node.js 模块系统
Node.js 模块系统 1. 引言 Node.js,作为一个轻量级、高效的服务器端 JavaScript 运行环境,其模块系统是其最核心的特性之一。Node.js 的模块系统允许开发者将代码组织成多个文件,每个文件都是一个模块,这样可以提高代码的可维护性和可重用性。本文将详细介绍 Node.js 的模…...
数据结构知识收集尊享版(迅速了解回顾相关知识)
1、单链表、循环链表、双向链表,存储、逻辑结构 单链表、循环链表和双向链表都是线性表的链式存储结构,它们在存储和逻辑结构上有一些共同点和不同点。 存储结构 单链表:每个节点包含一个数据域和一个指针域,指针域指向下一个节…...

SpringMVC启动与请求处理流程解析
目录 SpringMVC的基本结构 1.MVC简介 2.基本结构 什么是Handler? 什么是HandlerMapping? 什么是HandlerAdapter? RequestMapping方法参数解析 DispatcherServlet的init()方法 DispatcherServlet的doService()方法 SpringBoot整合SpringMVC …...

C++ 日志库 spdlog 使用教程
Spdlog是一个快速、异步、线程安全的C日志库,他可以方便地记录应用程序的运行状态,并提供多种输出格式。官网:https://github.com/gabime/spdlog 安装教程可以参考:https://blog.csdn.net/Harrytsz/article/details/144887297 S…...
`http_port_t
http_port_t 是 SELinux(Security-Enhanced Linux)中的一种端口类型标签,用于标识哪些端口可以被 HTTP 和 HTTPS 服务使用。SELinux 是一种强制访问控制(MAC)安全模块,它通过定义安全策略来限制进程对系统资…...

SpringBoot中实现拦截器和过滤器
【SpringBoot中实现过滤器和拦截器】 1.过滤器和拦截器简述 过滤器Filter和拦截器Interceptor,在功能方面很类似,但在具体实现方面差距还是比较大的。 2.过滤器的配置 2.1 自定义过滤器,实现Filter接口(SpringBoot 3.0 开始,jak…...
不锈钢均温板结合强力粘合技术革新手机内部架构
摘要: 本文介绍了一种创新性的手机内部架构设计方案,其中不锈钢均温板不仅作为高效的散热元件,还充当了手机中框的主要结构件。通过使用强力不可拆胶水将主板、尾插和其他关键部件直接粘合到均温板上,该方案实现了更为紧密的热耦合…...

Docker安装使用
文章目录 Docker安装Docker的基础使用搜索&拉取镜像 Docker的生命周期利用Docker切换不同OSDocker容器 镜像的保存&分享Docker存储Docker网络 Docker安装 更新apt索引 sudo apt-get update添加Docker所需要的依赖 apt-get install ca-certificates curl gnupg lsb-r…...
React 如何进行路由变化监听
一、使用react-router库(以react-router-dom为例) 1. 历史(history)对象监听 1.1 原理 react-router内部使用history对象来管理路由历史记录。可以通过访问history对象来监听路由变化。在基于类的组件中,可以通过组…...

Unity UGUI使用技巧与经验总结(不定期更新)
Text自动缩放参考连接: Unity -UGUI中Text文本框的自动调整,字体大小的自适应调节_unity添加的字体大小锁定-CSDN博客 Toggle按钮选择时,显示对应的UI界面: 为Toggle组件的On Value Change事件添加对需要显示的对象的SetActive…...

测试微信模版消息推送
进入“开发接口管理”--“公众平台测试账号”,无需申请公众账号、可在测试账号中体验并测试微信公众平台所有高级接口。 获取access_token: 自定义模版消息: 关注测试号:扫二维码关注测试号。 发送模版消息: import requests da…...

Spark 之 入门讲解详细版(1)
1、简介 1.1 Spark简介 Spark是加州大学伯克利分校AMP实验室(Algorithms, Machines, and People Lab)开发通用内存并行计算框架。Spark在2013年6月进入Apache成为孵化项目,8个月后成为Apache顶级项目,速度之快足见过人之处&…...

黑马Mybatis
Mybatis 表现层:页面展示 业务层:逻辑处理 持久层:持久数据化保存 在这里插入图片描述 Mybatis快速入门 
Java-41 深入浅出 Spring - 声明式事务的支持 事务配置 XML模式 XML+注解模式
点一下关注吧!!!非常感谢!!持续更新!!! 🚀 AI篇持续更新中!(长期更新) 目前2025年06月05日更新到: AI炼丹日志-28 - Aud…...
vue3 定时器-定义全局方法 vue+ts
1.创建ts文件 路径:src/utils/timer.ts 完整代码: import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...
CSS设置元素的宽度根据其内容自动调整
width: fit-content 是 CSS 中的一个属性值,用于设置元素的宽度根据其内容自动调整,确保宽度刚好容纳内容而不会超出。 效果对比 默认情况(width: auto): 块级元素(如 <div>)会占满父容器…...
为什么要创建 Vue 实例
核心原因:Vue 需要一个「控制中心」来驱动整个应用 你可以把 Vue 实例想象成你应用的**「大脑」或「引擎」。它负责协调模板、数据、逻辑和行为,将它们变成一个活的、可交互的应用**。没有这个实例,你的代码只是一堆静态的 HTML、JavaScript 变量和函数,无法「活」起来。 …...
BLEU评分:机器翻译质量评估的黄金标准
BLEU评分:机器翻译质量评估的黄金标准 1. 引言 在自然语言处理(NLP)领域,衡量一个机器翻译模型的性能至关重要。BLEU (Bilingual Evaluation Understudy) 作为一种自动化评估指标,自2002年由IBM的Kishore Papineni等人提出以来,…...
C语言中提供的第三方库之哈希表实现
一. 简介 前面一篇文章简单学习了C语言中第三方库(uthash库)提供对哈希表的操作,文章如下: C语言中提供的第三方库uthash常用接口-CSDN博客 本文简单学习一下第三方库 uthash库对哈希表的操作。 二. uthash库哈希表操作示例 u…...
验证redis数据结构
一、功能验证 1.验证redis的数据结构(如字符串、列表、哈希、集合、有序集合等)是否按照预期工作。 2、常见的数据结构验证方法: ①字符串(string) 测试基本操作 set、get、incr、decr 验证字符串的长度和内容是否正…...