【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
结束

未沸

未沸

沸腾

下面是巧克力锅炉的定义,在执行 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
-Singleton uniqueInstance
-usefulSingletonData
-Singleton()
+getInstance() : Singleton
+usefulSingletonMethod()
  1. 私有(或保护)构造函数 Singleton(),确保外部无法直接实例化;
  2. 私有(或保护)静态成员变量 uniqueInstance,引用 Singleton 的唯一实例;
  3. 公共静态成员方法 getInstance(),提供访问 Singleton 唯一实例的全局接口;
  4. 业务相关的数据 usefulSingletonData 和方法 usefulSingletonMethod(),用于实现类的具体功能。

单例模式的优点:

  1. 控制实例访问
    通过 getInstance() 方法,可以严格控制实例的访问时机和访问方式。
  2. 相比全局变量的优势
    • 实例唯一性:单例可以保证实例唯一,而全局变量无法保证;
    • 延迟初始化:单例可以根据需要创建实例,而全局变量在程序启动时就会创建(无论是否会用到);
    • 命名空间占用:单例的实例被封装在类内部,而全局变量直接占用全局命名空间(命名空间污染)。
  3. 实例数量可变
    通过修改 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线程2uniqueInstance
1ChocolateBoiler.getInstance()null
2if (uniqueInstance == null)null
3挂起ChocolateBoiler.getInstance()null
4if (uniqueInstance == null)null
5uniqueInstance = new ChocolateBoiler();object1
6return uniqueInstance;object1
7uniqueInstance = new ChocolateBoiler();object2
8return uniqueInstance;object2

在多线程环境下,果然有可能创建两个 ChocolateBoiler 实例 object1object2

为了确保实例的创建是线程安全的,我们有多种可选方案,包括同步方法、急切实例化、双重检查锁定等。

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

同步方法实现简单,但是也有明显的缺点:

  1. 运行时开销大:同步一个方法可能会使性能下降 100 倍(synchronizing a method can decrease performance by a factor of 100);
  2. 存在不必要的资源浪费:实际上,只有第一次执行 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(); 语句,其包含的步骤示意如下:

  1. memory = allocate(sizeof(Singleton.class)); 在堆内存中为对象分配空间
  2. construct(memory, Singleton.class); 在分配的内存空间上调用构造函数来初始化对象
  3. 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 中,使用单例还有一些其它注意事项:

  1. 类加载器问题
    • 问题描述:如果有两个或多个类加载器,就可以多次加载同一个类(每个类加载器一次)。如果这个类刚好是一个单例,就会有多于一个的实例。
    • 解决办法:确保单例通过同一个类加载器加载,通常使用系统类加载器(即启动类加载器)加载单例。
  2. 反射问题
    • 问题描述:通过反射可以调用类的私有构造方法,因此可能会创建类的多个实例。
    • 解决办法:在构造方法中添加防御性代码,防止通过反射创建多个实例。例如,可以在构造方法中检查是否已经存在实例,如果存在则抛出异常。
  3. 序列化和反序列化问题
    • 问题描述:当单例实现了 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 基础回顾

  1. 抽象(Abstraction)
  2. 封装(Encapsulation)
  3. 继承(Inheritance)
  4. 多态(Polymorphism)

5.2 OO 原则

5.2.1 新原则

本章没有介绍新的 OO 原则。

5.2.2 原则回顾

  1. 封装变化。
    Encapsulate what varies.
  2. 针对接口编程,而不是针对实现编程。
    Program to interfaces, not implementations.
  3. 优先使用组合,而不是继承。
    Favor composition over inheritance.
  4. 尽量做到交互对象之间的松耦合设计。
    Strive for loosely coupled designs between objects that interact.
  5. 类应该对扩展开放,对修改关闭。
    Classes should be open for extension, but closed for modification.
  6. 依赖抽象,不依赖具体类。
    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.

  1. 工厂方法(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.
  2. 抽象工厂(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.

  1. 装饰者模式(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.

  1. 策略模式(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.
  2. 观察者模式(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.

参考

  1. [美]弗里曼、罗布森著,UMLChina译.Head First设计模式.中国电力出版社.2022.2
  2. [美]伽玛等著,李英军等译.设计模式:可复用面向对象软件的基础.机械工业出版社.2019.3
  3. wickedlysmart: Head First设计模式 Java 源码
  4. [加]布迪·克尼亚万著,沈泽刚译.Java经典入门指南.人民邮电出版社.2020.6

Hi, I’m the ENDing, nice to meet you here! Hope this article has been helpful.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/66267.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【FlutterDart】页面切换 PageView PageController(9 /100)

上效果&#xff1a; 有些不能理解官方例子里的动画为什么没有效果&#xff0c;有可能是我写法不对 后续如果有动画效果修复了&#xff0c;再更新这篇&#xff0c;没有动画效果&#xff0c;总觉得感受的丝滑效果差了很多 上代码&#xff1a; import package:flutter/material.…

Electron快速入门——跨平台桌面端应用开发框架

个人简介 &#x1f440;个人主页&#xff1a; 前端杂货铺 &#x1f64b;‍♂️学习方向&#xff1a; 主攻前端方向&#xff0c;正逐渐往全干发展 &#x1f4c3;个人状态&#xff1a; 研发工程师&#xff0c;现效力于中国工业软件事业 &#x1f680;人生格言&#xff1a; 积跬步…

Android NDK开发实战之环境搭建篇(so库,Gemini ai)

文章流程 音视频安卓开发首先涉及到ffmpeg编译打包动态库&#xff0c;先了解动态库之间的cpu架构差异性。然后再搭建可运行的Android 环境。 So库适配 ⽇常开发我们经常会使⽤到第三库&#xff0c;涉及到底层的语⾳&#xff0c;视频等都需要添加so库。⽽so库的体积⼀般来说 ⾮…

【Java回顾】Day2 正则表达式----异常处理

参考资料&#xff1a;菜鸟教程 https://www.runoob.com/java/java-exceptions.html 正则表达式 有一部分没看完 介绍 字符串的模式搜索、编辑或处理文本java.util.regex包&#xff0c;包含了pattern和mathcer类&#xff0c;用于处理正则表达式的匹配操作。 捕获组 把多个字符…

Unity性能优化总结

目录 前言 移动端常见性能优化指标​编辑 包体大小优化 FPS CPU占用率 GPU占用率 内存 发热和耗电量 流量优化 前言 终于有时间了&#xff0c;我将在最近两个项目中进行优化的一些经验进行归纳总结以飨读者。因为我习惯用思维导图&#xff0c;所以归纳的内容主要以图来…

北京航空航天大学惊现技术商业“宫斗剧”!背后隐藏的内幕遭曝光!

北京航空航天大学&#xff08;以下称北航&#xff09;与源亿&#xff08;北京&#xff09;网络科技有限公司&#xff08;以下称源亿&#xff09;的派驻的员工恶意串通&#xff0c;指定北京蚂蚁非标科技有限公司&#xff08;以下称蚂蚁公司&#xff09;挖走源亿公司在现场派驻的…

transfomer深度学习实战水果识别

本文采用RT-DETR作为核心算法框架&#xff0c;结合PyQt5构建用户界面&#xff0c;使用Python3进行开发。RT-DETR以其高效的实时检测能力&#xff0c;在多个目标检测任务中展现出卓越性能。本研究针对水果数据集进行训练和优化&#xff0c;该数据集包含丰富的水果图像样本&#…

TensorFlow深度学习实战(3)——深度学习中常用激活函数详解

TensorFlow深度学习实战&#xff08;3&#xff09;——深度学习中常用激活函数详解 0. 前言1. 引入激活函数1.1 感知器1.2 多层感知器1.3 训练感知器存在的问题 2. 激活函数3. 常见激活函数3.1 sigmoid3.2 tanh3.3 ReLU3.4 ELU和Leaky ReLU 小结系列链接 0. 前言 使用激活函数…

数据结构C语言描述9(图文结合)--二叉树和特殊书的概念,二叉树“最傻瓜式创建”与前中后序的“递归”与“非递归遍历”

前言 这个专栏将会用纯C实现常用的数据结构和简单的算法&#xff1b;有C基础即可跟着学习&#xff0c;代码均可运行&#xff1b;准备考研的也可跟着写&#xff0c;个人感觉&#xff0c;如果时间充裕&#xff0c;手写一遍比看书、刷题管用很多&#xff0c;这也是本人采用纯C语言…

Leetcode打卡:设计一个ATM机器

执行结果&#xff1a;通过 题目 2241 设计一个ATM机器 一个 ATM 机器&#xff0c;存有 5 种面值的钞票&#xff1a;20 &#xff0c;50 &#xff0c;100 &#xff0c;200 和 500 美元。初始时&#xff0c;ATM 机是空的。用户可以用它存或者取任意数目的钱。 取款时&#xff0c…

vscode如何离线安装插件

在没有网络的时候,如果要安装插件,就会麻烦一些,需要通过离线安装的方式进行。下面记录如何在vscode离线安装插件。 一、下载离线插件 在一台能联网的电脑中,下载好离线插件,拷贝到无法联网的电脑上。等待安装。 vscode插件商店地址:https://marketplace.visualstudio.co…

用Tkinter制作一个用于合并PDF文件的小程序

需要安装PyPDF2库&#xff0c;具体原代码如下&#xff1a; # -*- coding: utf-8 -*- """ Created on Sun Dec 29 14:44:20 2024author: YBK """import PyPDF2 import os import tkinter as tk import windndpdf_files [] def dragged_files(f…

CDP集成Hudi实战-Hive

[〇]关于本文 本文测试一下使用Hive和Hudi的集成 软件版本Hudi1.0.0Hadoop Version3.1.1.7.3.1.0-197Hive Version3.1.3000.7.3.1.0-197Spark Version3.4.1.7.3.1.0-197CDP7.3.1 [一]部署Jar包 1-部署hudi-hive-sync-bundle-1.0.0.jar文件 [rootcdp73-1 ~]# for i in $(se…

公司资产网站

本文结尾处获取源码。 本文结尾处获取源码。 本文结尾处获取源码。 一、相关技术 后端&#xff1a;Java、JavaWeb / Springboot。前端&#xff1a;Vue、HTML / CSS / Javascript 等。数据库&#xff1a;MySQL 二、相关软件&#xff08;列出的软件其一均可运行&#xff09; I…

CSS——2.书写格式一

<!DOCTYPE html> <html><head><meta charset"UTF-8"><title></title></head><body><!--css书写中&#xff1a;--><!--1.css 由属性名:属性值构成--><!--style"color: red;font-size: 20px;&quo…

基于FPGA的辩论赛系统设计-8名选手-正反两方-支持单选手评分-正反两方评分总和

基于FPGA的辩论赛系统设计 功能描述一、系统概述二、仿真波形视频 功能描述 1.答辩倒计时功能&#xff0c;当正反任意一方开始答辩后&#xff0c;倒计时30S。在倒计时最后10S后&#xff0c;LED灯开始闪烁。 2.答辩评分和计分功能&#xff0c;当答辩方结束答辩后&#xff0c;评…

【OceanBase】使用 Superset 连接 OceanBase 数据库并进行数据可视化分析

文章目录 前言一、前提条件二、操作步骤2.1 准备云主机实例2.2 安装docker-compose2.3 使用docker-compose安装Superset2.3.1 克隆 Superset 的 GitHub 存储库2.3.2 通过 Docker Compose 启动 Superset 2.4 开通 OB Cloud 云数据库2.5 获取连接串2.6 使用 Superset 连接 OceanB…

打造三甲医院人工智能矩阵新引擎(二):医学影像大模型篇--“火眼金睛”TransUNet

一、引言 1.1 研究背景与意义 在现代医疗领域,医学影像作为疾病诊断与治疗的关键依据,发挥着不可替代的作用。从传统的X射线、CT(计算机断层扫描)到MRI(磁共振成像)等先进技术,医学影像能够直观呈现人体内部结构,为医生提供丰富的诊断信息,涵盖疾病识别、病灶定位、…

计算机缺失x3daudio1 7.dll怎么修复?

电脑运行时常见问题解析与修复策略&#xff1a;以“x3daudio1_7.dll缺失”为例 在软件开发与日常电脑维护的广阔领域中&#xff0c;我们时常会遇到各种系统报错和文件问题。这些问题不仅影响我们的工作效率&#xff0c;还可能对数据安全构成潜在威胁。作为一位经验丰富的软件开…

WPF区域导航+导航参数使用+路由守卫+导航日志

背景&#xff1a;使用ContentControl控件实现区域导航是有Mvvm框架的WPF都能使用的&#xff0c;不限于Prism 主要是将ContenControl控件的Content内容在ViewModel中切换成不同的用户控件 下面是MainViewModel&#xff1a; private object body;public object Body {get { retu…