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

如何编写可检测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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.19 08:16:02