Java方法线程安全单元测试简洁实现方案及问题答疑
懒加载单例线程安全测试问题解答
1. CompletableFuture测试多线程场景的思路是否正确
这个思路本身没有概念误用,CompletableFuture本身就是用来执行异步任务、获取返回结果的,用来并行调用getInstance对比返回实例的方向是对的。但你的具体写法有两个没考虑周全的问题:
- 默认使用了JVM全局共享的
ForkJoinPool.commonPool(),这个线程池的资源是所有用到它的代码共用的,调度时序完全不受你控制,根本没法保证两个任务真的能并行执行到临界区代码 - 完全没有处理静态单例的状态重置问题,这是批量跑测试失败的核心诱因
2. 单测单独运行通过、批量运行失败的原理
本质是两个全局共享资源的不确定性导致的:
- 静态变量的状态污染:你定义的
dateUtilInstance是静态类变量,和类绑定,同一个类加载器下全局只有一份,不会在每个测试方法执行时自动重置回null。单独跑这个测试方法时,DateUtil刚被加载,静态变量还是初始的null值,两个任务同时进入if判断,经过1s休眠后各自创建实例,就能复现不安全问题;批量跑的时候,如果其他测试方法提前调用过DateUtil.getInstance(),单例早就被初始化好了,两个任务直接返回同一个实例,断言自然失败。 - 公共线程池的调度不确定性:默认的公共ForkJoinPool并行度和CPU核心数挂钩,批量跑测试时如果其他测试任务占了池里的工作线程,很可能出现第一个提交的任务先跑完、把单例初始化完了,第二个任务才拿到线程执行,根本进不了创建实例的分支,自然拿不到不同的实例。你加的1s sleep只是放大了空窗期,没法保证两个任务一定能同时进入临界区。
另外补充一点:你这里用assertNotEquals判断实例是否相同是错的,应该用assertNotSame——如果DateUtil重写了equals方法,哪怕是两个不同的实例,equals判断也可能返回true,会直接导致测试结果不准。
3. 更简洁规范的测试实现方案
要稳定复现这个非线程安全的问题,核心要解决两个问题:每个测试执行前把单例重置回null、保证两个线程能同时进入临界区代码。不需要额外引入复杂依赖,用JDK自带的CountDownLatch做线程同步就能实现稳定的测试,代码如下:
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.lang.reflect.Field; import java.util.concurrent.CountDownLatch; import static org.junit.jupiter.api.Assertions.assertNotSame; class DateUtilTest { // 每个测试执行前,通过反射把静态单例重置为null,避免测试间状态污染 @BeforeEach void resetSingleton() throws NoSuchFieldException, IllegalAccessException { Field instanceField = DateUtil.class.getDeclaredField("dateUtilInstance"); instanceField.setAccessible(true); instanceField.set(null, null); } @Test void shouldVerifyThatClassInstantiationIsThreadUnsafe() throws InterruptedException { // 两个闩锁分别控制线程统一启动、等待所有线程执行完成 CountDownLatch startGate = new CountDownLatch(1); CountDownLatch endGate = new CountDownLatch(2); DateUtil[] holder = new DateUtil[2]; Thread t1 = new Thread(() -> { try { startGate.await(); // 等放行信号,保证两个线程同时开始执行 holder[0] = DateUtil.getInstance(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { endGate.countDown(); } }); Thread t2 = new Thread(() -> { try { startGate.await(); holder[1] = DateUtil.getInstance(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { endGate.countDown(); } }); t1.start(); t2.start(); startGate.countDown(); // 统一放行两个线程,最大化同时进入临界区的概率 endGate.await(); // 等两个线程都执行完再做断言 assertNotSame(holder[0], holder[1]); } }
这个实现不需要额外创建自定义的Callable/Runnable实现类,线程同步逻辑完全可控,只要两个线程能同时进入if判断分支,就能稳定复现线程不安全的问题,不会出现批量运行失败的情况。
如果后续需要做大量并发场景测试,也可以用封装好的并发测试工具,核心逻辑和上面的写法一致,都是通过同步屏障控制线程的启动时序,同时做好测试间的状态隔离。
内容的提问来源于stack exchange,提问作者lifeLongLearner
相关产品推荐
相关产品推荐

