Menu
CoddyTech

线程安全的单例

本课属于 Coddy C# 学习旅程的“面向对象编程”单元。第 52 课,共 70 课。

单例模式可确保一个类在整个应用程序中只有一个实例,并提供全局访问该实例的入口。这适用于管理共享资源,例如配置设置、日志服务或数据库连接。

基础的单例使用私有构造函数来阻止外部实例化,并使用静态属性提供访问入口:

public class Logger
{
    private static Logger _instance;
    private static readonly object _lock = new object();
    
    private Logger() { }  // 私有构造函数
    
    public static Logger Instance
    {
        get
        {
            lock (_lock)
            {
                if (_instance == null)
                    _instance = new Logger();
                return _instance;
            }
        }
    }
    
    public void Log(string message) => Console.WriteLine(message);
}

lock 语句可确保线程安全。如果多个线程尝试同时访问 Instance,同一时间只能有一个线程进入临界区。如果没有这个语句,两个线程都可能看到 _instance 为 null,并创建各自的实例。

C# 提供了一种更简单的线程安全方法,使用静态初始化:

public class Logger
{
    private static readonly Logger _instance = new Logger();
    
    private Logger() { }
    
    public static Logger Instance => _instance;
    
    public void Log(string message) => Console.WriteLine(message);
}

这个版本是线程安全的,因为 CLR 保证静态字段只会初始化一次。这两种方法都能确保你始终获取同一个实例:

Logger.Instance.Log("First call");
Logger.Instance.Log("Same instance");
challenge icon

挑战

简单

让我们使用单例模式构建一个线程安全的配置管理器。在实际应用中,配置设置需要在整个程序中都能访问,同时还要确保只存在一个实例。这正是单例模式的理想使用场景。

你需要将代码分成两个文件:

  • ConfigManager.cs:在 Configuration 命名空间中创建一个实现单例模式的 ConfigManager 类。配置管理器应满足以下要求:
    • 有一个私有静态字段,用来保存唯一实例
    • 使用私有构造函数,防止外部创建实例
    • 提供一个静态 Instance 属性,用来返回唯一实例(使用更简单的静态初始化方式来确保线程安全)
    • 包含一个私有字符串字段 _appName,并将其初始化为 "MyApp"
    • 提供一个 SetAppName(string name) 方法,用来更新应用名称
    • 提供一个 GetAppName() 方法,用来返回当前应用名称
  • Program.cs:在主文件中,多次访问该实例并展示更改会在所有引用中保留,以证明单例模式能够正常工作。

你会收到一个输入:

  • 要设置的新应用名称(例如,ProductionServer)

在程序中:

  1. 获取 ConfigManager 实例,并打印默认应用名称
  2. 再次获取该实例(存入另一个变量),并使用它将输入的新应用名称设为应用名称
  3. 第三次获取该实例,并打印应用名称,以证明所有引用共享相同的数据

例如,如果输入是 ProductionServer,输出应为:

MyApp
ProductionServer

这展示了单例模式的核心优势:无论你访问 Instance 多少次,得到的始终是同一个对象,并且共享同一状态。静态初始化方式能够确保线程安全,因为即使多个线程同时访问,CLR 也会确保静态字段只初始化一次。

亲手试试

using System;
using Configuration;

class Program
{
    public static void Main(string[] args)
    {
        // 读取输入的新应用名称
        string newAppName = Console.ReadLine();
        
        // TODO:获取 ConfigManager 实例并打印默认应用名称
        
        // TODO:再次获取实例(存入另一个变量),用它设置新的应用名称
        
        // TODO:第三次获取实例并打印应用名称,证明所有引用共享同一份数据
    }
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: C# 在线编译器