如何在单元测试中测试单例模式(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), "并非所有单例都是同一个实例。"); }
问题在于:
- 线程数量不足,竞争概率太低,很难触发多线程同时初始化的场景;
- 单例的静态
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.
相关产品推荐
相关产品推荐

