如何编写可检测Java非线程安全单例的单元测试?
检测Java单例线程安全性的单元测试方案
没问题,咱们来搞定这个单例线程安全的测试问题!核心思路很简单:用大量线程同时抢着调用getInstance(),然后验证所有线程拿到的实例是否唯一——非线程安全的单例大概率会生成多个实例,而线程安全的只会有一个。
先明确两个测试对象
首先咱们先把两个单例类的代码摆出来,方便后续测试:
1. 非线程安全的懒汉单例
public class UnsafeSingleton { private static UnsafeSingleton instance; // 加个构造计数器,方便测试统计创建次数(实际生产单例别这么写) public static int creationCount = 0; private UnsafeSingleton() { creationCount++; } public static UnsafeSingleton getInstance() { if (instance == null) { // 这里存在竞态条件:多个线程可能同时进入这个分支 instance = new UnsafeSingleton(); } return instance; } }
2. 线程安全的同步锁单例
public class SafeSingleton { private static SafeSingleton instance; public static int creationCount = 0; private SafeSingleton() { creationCount++; } // 加了synchronized关键字,保证同一时间只有一个线程能进入方法 public static synchronized SafeSingleton getInstance() { if (instance == null) { instance = new SafeSingleton(); } return instance; } }
编写单元测试(用JUnit 5为例)
我们用ExecutorService创建大量线程模拟高并发,然后通过集合存储实例或者构造计数器来验证结果:
import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.*; public class SingletonThreadSafetyTest { // 非线程安全单例的测试:因为问题是概率性的,所以用@RepeatedTest重复跑几次 @RepeatedTest(10) void testUnsafeSingletonCreatesMultipleInstances() throws InterruptedException { // 重置计数器,避免之前测试的影响 UnsafeSingleton.creationCount = 0; int threadCount = 2000; // 足够多的线程增加竞态条件触发概率 Set<UnsafeSingleton> instanceSet = new HashSet<>(); ExecutorService executor = Executors.newFixedThreadPool(threadCount); // 让所有线程同时调用getInstance for (int i = 0; i < threadCount; i++) { executor.submit(() -> { UnsafeSingleton instance = UnsafeSingleton.getInstance(); // 同步集合操作,避免并发修改异常 synchronized (instanceSet) { instanceSet.add(instance); } }); } // 关闭线程池并等待所有线程执行完成 executor.shutdown(); assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS), "线程未在规定时间内完成"); // 验证结果:非线程安全的话,实例数量和构造次数都会大于1 System.out.println("UnsafeSingleton 实例数量:" + instanceSet.size() + ",构造次数:" + UnsafeSingleton.creationCount); assertTrue(instanceSet.size() > 1 || UnsafeSingleton.creationCount > 1, "非线程安全单例应该生成多个实例"); } // 线程安全单例的测试 @Test void testSafeSingletonCreatesOnlyOneInstance() throws InterruptedException { SafeSingleton.creationCount = 0; int threadCount = 2000; Set<SafeSingleton> instanceSet = new HashSet<>(); ExecutorService executor = Executors.newFixedThreadPool(threadCount); for (int i = 0; i < threadCount; i++) { executor.submit(() -> { SafeSingleton instance = SafeSingleton.getInstance(); synchronized (instanceSet) { instanceSet.add(instance); } }); } executor.shutdown(); assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS), "线程未在规定时间内完成"); System.out.println("SafeSingleton 实例数量:" + instanceSet.size() + ",构造次数:" + SafeSingleton.creationCount); assertEquals(1, instanceSet.size(), "线程安全单例应该只生成一个实例"); assertEquals(1, SafeSingleton.creationCount, "线程安全单例的构造函数应该只执行一次"); } }
关键测试细节说明
- 概率性问题处理:非线程安全的竞态条件不是每次都会触发,所以用
@RepeatedTest重复跑测试,或者增加线程数量(比如2000个),在多核CPU上运行的话触发概率会更高。 - 线程同步:对
HashSet的add操作要加同步锁,不然会抛出ConcurrentModificationException,或者直接用线程安全的集合比如CopyOnWriteArraySet也可以。 - 等待线程完成:必须用
awaitTermination确保所有线程都执行完再检查结果,不然可能拿到不完整的统计数据。 - 计数器辅助验证:在构造函数里加计数器比只看实例集合更直观,因为有时候可能因为JVM的对象引用优化,集合里的实例看起来是一个,但实际上构造函数已经执行了多次。
测试结果预期
- 运行
testUnsafeSingletonCreatesMultipleInstances:大部分情况下会触发断言失败(或者打印的实例数/构造次数大于1),证明单例线程不安全。 - 运行
testSafeSingletonCreatesOnlyOneInstance:无论跑多少次,实例数和构造次数都会是1,证明线程安全。
内容的提问来源于stack exchange,提问作者MickeyThreeSheds
相关产品推荐
相关产品推荐

