You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在单元测试中测试单例模式(Singleton)的线程安全性?

如何编写能暴露单例线程安全问题的TDD测试?

背景:非线程安全的单例实现

大家都熟悉单例模式,以下是一个常见的非线程安全实现:

public class MySingleton
{
    private MySingleton() { }

    public static MySingleton GetInstance()
    {
        return instance ??= new MySingleton();
    }

    private static MySingleton? instance;
}

这个实现存在线程安全问题,但问题难以复现。按照TDD流程,需要先编写一个会失败的测试,再修复代码让测试通过,但现有测试始终无法触发失败。

现有测试的局限

我最初编写的测试是启动与处理器核心数相等的线程来获取实例,但测试始终显示"绿态":

[Test]
public void CheckThreadSafety_TooSimple()
{
    // Arrange: 启动多个线程
    var threadCount = Environment.ProcessorCount;
    var singletons = new MySingleton?[threadCount];
    var threads = new Thread[threadCount];
    for (var i = 0; i < threadCount; i++)
    {
        var captured = i;
        threads[i] = new(() =>
        {
            singletons[captured] = MySingleton.GetInstance();
        });
        threads[i].Start();
    }

    // Act: 等待所有线程完成
    foreach (var thread in threads) thread.Join();

    // Assert: 所有实例必须相同
    var finalInstance = MySingleton.GetInstance();
    Assert.IsTrue(singletons.All(s => s == finalInstance), "并非所有单例都是同一个实例。");
}

问题在于:

  1. 线程数量不足,竞争概率太低,很难触发多线程同时初始化的场景;
  2. 单例的静态instance字段一旦初始化,后续测试无法重置状态,循环重复测试也无效:
for (int repeat = 0; repeat < 100; repeat++)
{
    // 无效:首次运行后单例已完成初始化
}

可复现失败的测试方案

要写出能稳定失败的测试,需要解决两个核心问题:重置单例状态和放大线程竞争概率。

1. 用反射重置单例状态

由于单例的instance是私有静态字段,我们可以通过反射在每次测试前将其重置为null,确保每次测试都从初始状态开始:

private void ResetSingletonInstance()
{
    var instanceField = typeof(MySingleton).GetField(
        "instance", 
        BindingFlags.NonPublic | BindingFlags.Static
    );
    instanceField.SetValue(null, null);
}

2. 放大线程竞争概率

使用Barrier让所有线程同时开始获取实例,避免线程分批启动导致的竞争不足;同时增加线程数量,并在单例构造函数中添加短暂延迟,放大线程切换的概率:

先修改单例构造函数(仅用于测试阶段,修复后可移除):

private MySingleton() 
{
    Thread.Sleep(1); // 增加初始化耗时,更容易触发线程安全问题
}

然后编写改进后的测试:

[Test]
public void CheckThreadSafety_ShouldFailWithUnsafeImplementation()
{
    // 每次测试前重置单例,保证初始状态
    ResetSingletonInstance();

    const int threadCount = 200; // 增加线程数量提升竞争概率
    var singletons = new List<MySingleton>();
    var barrier = new Barrier(threadCount); // 同步所有线程的启动时机
    var countdown = new CountdownEvent(threadCount); // 可靠等待所有线程完成

    for (var i = 0; i < threadCount; i++)
    {
        new Thread(() =>
        {
            barrier.SignalAndWait(); // 等待所有线程准备就绪
            var instance = MySingleton.GetInstance();
            
            lock (singletons) // 线程安全地收集实例
            {
                singletons.Add(instance);
            }
            
            countdown.Signal(); // 标记当前线程完成
        }).Start();
    }

    countdown.Wait(); // 等待所有线程执行完毕

    // 断言:所有实例必须是同一个
    var distinctInstanceCount = singletons.Distinct().Count();
    Assert.That(
        distinctInstanceCount, 
        Is.EqualTo(1), 
        $"线程不安全:创建了{distinctInstanceCount}个不同的单例实例"
    );
}

这个测试会稳定触发非线程安全实现的问题,测试结果会显示失败(红态)。当你用加锁的线程安全实现替换原代码后,测试会变为通过(绿态)。

线程安全的修复实现如下(供参考):

public class MySingleton
{
    private MySingleton() { }

    public static MySingleton GetInstance()
    {
        lock (lockObject)
        {
            return instance ??= new MySingleton();
        }
    }

    private static MySingleton? instance;
    private static readonly object lockObject = new();
}

内容的提问来源于stack exchange,提问作者Thomas W.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 11:45:15