当前位置: 首页 > article >正文

【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;  // 在类加载时被初始化为 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
-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() {
            @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 基础回顾

  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.kler.cn/a/469229.html

相关文章:

  • 【Linux】传输层协议UDP
  • 微服务-Eureka
  • conda安装及demo:SadTalker实现图片+音频生成高质量视频
  • 网络协议安全的攻击手法
  • 【开源工业视觉库】启航规划
  • 深入解析-正则表达式
  • 深入Android架构(从线程到AIDL)_09 认识Android的主线程
  • MATLAB R2015b安装、激活记录少走弯路
  • 【Unity Shader】【图形渲染】Unity Shader操作基础5-Unity Shader调试技巧
  • 面向实习的Golang服务端技能分析
  • MATLAB语言的函数实现
  • [桌面运维]windows自动设置浅深色主题
  • 基于Springboot +Vue 实验课程预约管理系统
  • [CTF/网络安全] 攻防世界 simple_php 解题详析
  • 决策树和随机森林
  • 云手机 —— 手机矩阵的 “超级外挂
  • JAVA解析Excel复杂表头
  • HTML——66.单选框
  • Unity3D 搭建ILRuntime开发环境详解
  • security框架的安全登录
  • 【Cesium】九、Cesium点击地图获取点击位置的坐标,并在地图上添加图标
  • Chrome 查看 session 信息
  • 后端Java开发:第七天
  • ref() 和 reactive() 区别
  • Genome Research | 俄亥俄州立于忠堂组-结合深度学习与蛋白质数据库系统探究反刍动物真核微生物...
  • 解决docker: Error response from daemon: Get “https://registry-1.docker.io/v2/“: net/http: request canc