【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.

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

嵌入式linux系统中QT信号与槽实现

第一:Qt中信号与槽简介 信号与槽是Qt编程的基础。因为有了信号与槽的编程机制,在Qt中处理界面各个组件的交互操作时变得更加直观和简单。 槽函数与一般的函数不同的是:槽函数可以与一个信号关联,当信号被发射时,关联的槽函数被自动执行。 案例操作与实现: #ifndef …

php有两个数组map比较 通过id关联,number可能数量变化 比较他们之间增加修改删除

在PHP中&#xff0c;比较两个通过ID关联的数组&#xff0c;并确定它们之间的增加、修改和删除操作&#xff0c;你可以使用以下步骤&#xff1a; 创建两个数组&#xff1a;假设你有两个数组&#xff0c;分别表示“旧数据”和“新数据”。使用ID作为键&#xff1a;为了方便比较&a…

C++和OpenGL实现3D游戏编程【连载19】——着色器光照初步(平行光和光照贴图)(附源码)

1、本节要实现的内容 我们在前期的教程中,讨论了在即时渲染模式下的光照内容。但在我们后期使用着色器的核心模式下,会经常在着色器中使光照,我们这里就讨论一下着色器光照效果,以及光照贴图效果,同时这里知识会为后期的更多光照效果做一些铺垫。本节我们首先讨论冯氏光照…

《learn_the_architecture_-_generic_interrupt_controller_v3_and_v4__overview》学习笔记

1.GIC是基于Arm GIC架构实现的&#xff0c;该架构已经从GICv1发展到最新版本GICv3和GICv4。 Arm 拥有多个通用中断控制器&#xff0c;可为所有类型的 Arm Cortex 多处理器系统提供一系列中断管理解决方案。这些控制器的范围从用于具有小型 CPU 内核数的系统的最简单的 GIC-400 …

健身房管理系统多身份

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

General OCR Theory: Towards OCR-2.0 via a Unified End-to-end Model

通用 OCR 理论&#xff1a;通过统一的端到端模型实现 OCR-2.0 Abstract 随着人们对人工光学字符的智能处理需求日益增长&#xff0c;传统的OCR系统&#xff08;OCR-1.0&#xff09;已越来越不能满足人们的使用需求。本文&#xff0c;我们将所有人工光学信号&#xff08;例如纯…

大数据组件(二)快速入门数据集成平台SeaTunnel

大数据组件(二)快速入门数据集成平台SeaTunnel SeaTunnel是一个超高性能的分布式数据集成平台&#xff0c;支持实时海量数据同步。 每天可稳定高效同步数百亿数据&#xff0c;已被近百家企业应用于生产。 SeaTunnel的运行流程如下图所示&#xff1a; 工作流程为&#xff1a;So…

前端如何判断多个请求完毕

在前端开发中&#xff0c;经常会遇到需要同时发起多个异步请求&#xff0c;并在所有请求都完成后再进行下一步操作的情况。 这里有几个常用的方法来实现这一需求&#xff1a; 使用 Promise.all() Promise.all() 方法接收一个 Promise 对象的数组作为参数&#xff0c;当所有的…

【机器学习】穷理至极,观微知著:微积分的哲思之旅与算法之道

文章目录 微积分基础&#xff1a;理解变化与累积的数学前言一、多重积分的高级应用1.1 高维概率分布的期望值计算1.1.1 多维期望值的定义1.1.2 Python代码实现1.1.3 运行结果1.1.4 结果解读 1.2 特征空间的体积计算1.2.1 单位球体的体积计算1.2.2 Python代码实现1.2.3 运行结果…

基于Arduino的FPV头部追踪相机系统

构建FPV头部追踪相机&#xff1a;让你置身于遥控车辆之中&#xff01; 在遥控车辆和模型飞行器的世界中&#xff0c;第一人称视角&#xff08;FPV&#xff09;体验一直是爱好者们追求的目标。通过FPV头部追踪相机&#xff0c;你可以像坐在车辆或飞行器内部一样&#xff0c;自由…

鸿蒙HarmonyOS开发:拨打电话、短信服务、网络搜索、蜂窝数据、SIM卡管理、observer订阅管理

文章目录 一、call模块&#xff08;拨打电话&#xff09;1、使用makeCall拨打电话2、获取当前通话状态3、判断是否存在通话4、检查当前设备是否具备语音通话能力 二、sms模块&#xff08;短信服务&#xff09;1、创建短信2、发送短信 三、radio模块&#xff08;网络搜索&#x…

高校教务系统登录页面JS分析——安徽大学

高校教务系统密码加密逻辑及JS逆向 最近有粉丝说安徽大学的教务系统换了&#xff0c;之前用的是正方出品的系统&#xff0c;今天我来看看新版教务系统怎么模拟登录&#xff0c;总体来说&#xff0c;还是比较简单的&#xff0c;就是一个哈希加密了密码&#xff0c;其次就是一个滑…

在CodeBlocks搭建SDL2工程构建TFT彩屏模拟器虚拟TFT彩屏幕显示

在CodeBlocks搭建SDL2工程构建TFT彩屏模拟器虚拟TFT彩屏幕显示 参考文章源码下载地址一、SDL2的创建、初始化、退出二、系统基本Tick、彩屏刷新、按键事件三、彩屏获取与设置颜色四、彩屏填充颜色及清屏五、彩屏显示中文和英文字符串六、彩屏显示数字七、彩屏初始化八、主函数测…

Speech Recognition vs. Voice Recognition | 语音识别工作原理 | 模型训练 | 应用

注&#xff1a;机翻&#xff0c;未校。 Speech Recognition 与 Voice Recognition 剑桥词典 speech recognition&#xff0c;语音识别 voice recognition&#xff0c;声音识别 Speech vs. Voice - What’s the Difference? | This vs. That https://thisvsthat.io/speech-vs…

《Vue3实战教程》35:Vue3测试

如果您有疑问&#xff0c;请观看视频教程《Vue3实战教程》 测试​ 为什么需要测试​ 自动化测试能够预防无意引入的 bug&#xff0c;并鼓励开发者将应用分解为可测试、可维护的函数、模块、类和组件。这能够帮助你和你的团队更快速、自信地构建复杂的 Vue 应用。与任何应用一…

【MySQL 保姆级教学】用户管理和数据库权限(16)

数据库账户管理是指对数据库用户进行创建、修改和删除等操作&#xff0c;以控制用户对数据库的访问权限。通过账户管理&#xff0c;可以设置用户名、密码、主机地址等信息&#xff0c;确保数据库的安全性和可控性。例如&#xff0c;使用 CREATE USER 创建用户&#xff0c;ALTER…

【复盘】2024年终总结

工作 重构风控系统 今年上半年其实就是整体重构系统&#xff0c;经历了多次加班的&#xff0c;其中的辛酸苦辣只有自己知道&#xff0c;现在来看的话&#xff0c;其实对自己还有一定的成长&#xff0c;从这件事情上也明白 绩效能不能拿到A&#xff0c;在分配的任务的时候就决…

美食烹饪互动平台

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

linux-centos-安装miniconda3

参考&#xff1a; 最新保姆级Linux下安装与使用conda&#xff1a;从下载配置到使用全流程_linux conda-CSDN博客 https://blog.csdn.net/qq_51566832/article/details/144113661 Linux上删除Anaconda或Miniconda的步骤_linux 删除anaconda-CSDN博客 https://blog.csdn.net/m0_…

[读书日志]从零开始学习Chisel 第一篇:书籍介绍,Scala与Chisel概述,Scala安装运行(敏捷硬件开发语言Chisel与数字系统设计)

简介&#xff1a;从20世纪90年代开始&#xff0c;利用硬件描述语言和综合技术设计实现复杂数字系统的方法已经在集成电路设计领域得到普及。随着集成电路集成度的不断提高&#xff0c;传统硬件描述语言和设计方法的开发效率低下的问题越来越明显。近年来逐渐崭露头角的敏捷化设…