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

TestNG并行Selenium测试资源调度死锁问题排查与解决

问题背景

我用TestNG做Selenium测试的并行执行,有部分测试需要独占资源(同一资源不能被多个测试同时使用),现有10个这类资源。测试获取资源后可用数减1,资源全被占用时新测试得等已有测试释放资源才能获取。

我写了ResourceProvider类管控资源逻辑,包含两个核心方法:

  • get():测试方法里调用,用来获取资源
  • remove():通过实现RemoveResource接口的@AfterMethod默认方法调用,用来释放资源

但现在代码有时会在remove()调用处挂起,疑似死锁,求帮忙排查原因并给出解决方案。

相关代码如下:

ResourceProvider类

public class ResourceProvider {
private static final Logger logger = LogManager.getLogger();
private static List<Resource> freeResources;
private static Map<String, List<Resource>> resourcesInUse;

static {
    freeResources = new ArrayList<>();
    // 这里初始化资源填充freeResources列表
    resourcesInUse = new HashMap<>();
}

public static synchronized Resource get() {
    String testName = Thread.currentThread().getStackTrace()[2].getClassName()
            + "." + Thread.currentThread().getStackTrace()[2].getMethodName();
    Resource resource = null;
    logger.info(String.format("Attempt to get resource for %s test", testName));

    for (int i = 0; i < 240; i++) {
        if (freeResources.isEmpty()) {
            try {
                Thread.sleep(5_000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            resource = freeResources.get(0);

            if (resourcesInUse.containsKey(testName)) {
                resourcesInUse.get(testName).add(resource);
            } else {
                List<Resource> resources = new ArrayList<>();
                resources.add(resource);
                resourcesInUse.put(testName, resources);
            }

            freeResources.remove(resource);
            break;
        }
    }
    if (resource == null) {
        throw new RuntimeException(String.format("There is no free resource for '%s' in 20 minutes", testName));
    }
    logger.info(String.format("Resource %s used in %s", resource, testName));
    return resource;
}

public static synchronized boolean remove(ITestResult result) {
    String testName = result.getMethod().getTestClass().getName() + "." + result.getMethod().getMethodName();
    return remove(testName);
}

public static synchronized boolean remove(String testName) {
    boolean isTestUseResource = resourcesInUse.containsKey(testName);
    if (isTestUseResource) {
        logger.info(String.format("Removing %s resources, used in %s", resourcesInUse.get(testName), testName));
        freeResources.addAll(resourcesInUse.get(testName));
        resourcesInUse.remove(testName);
    }
    return isTestUseResource;
}
}

RemoveResource接口

public interface RemoveResource {

@AfterMethod
default void removeResource(ITestResult result) {
    ResourceProvider.remove(result);
}
}
问题原因排查
  1. 同步锁下的sleep导致锁持有:get()和remove()都是静态同步方法,本质上抢的是ResourceProvider类对象的锁。当资源耗尽时,get()里的线程会进入sleep(),但此时它还握着锁,remove()方法根本拿不到锁去释放资源,直接导致死锁——等待资源的线程占着锁sleep,释放资源的线程拿不到锁没法干活,资源永远无法释放。
  2. 测试名获取不可靠:用Thread.currentThread().getStackTrace()[2]拿测试类和方法名的方式太依赖栈帧结构,不同JVM、不同调用场景下栈帧索引可能变化,导致测试名识别错误,资源绑定混乱,进一步加剧锁占用问题。
  3. 非线程安全集合隐患:ArrayList和HashMap本身不是线程安全的,就算方法加了synchronized,并发场景下仍可能出现集合内部结构不一致的问题,间接引发锁相关的异常。
解决方案

1. 用等待/通知机制替换sleep

放弃synchronized,改用ReentrantLock配合Condition实现等待/通知,资源不足时释放锁等待,有资源释放时唤醒等待线程,避免锁被长时间占用。

2. 替换为线程安全集合

用CopyOnWriteArrayList存空闲资源,ConcurrentHashMap存已占用资源,从集合层面保证线程安全。

3. 可靠绑定测试与资源

去掉栈帧获取测试名的逻辑,改用TestNG提供的ITestResult或测试方法主动传递明确的测试标识,确保资源和测试的绑定准确。

修正后的完整代码

ResourceProvider类

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.ITestResult;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ResourceProvider {
    private static final Logger logger = LoggerFactory.getLogger(ResourceProvider.class);
    private static final List<Resource> freeResources = new CopyOnWriteArrayList<>();
    private static final Map<String, List<Resource>> resourcesInUse = new ConcurrentHashMap<>();
    private static final ReentrantLock lock = new ReentrantLock();
    private static final Condition resourceAvailable = lock.newCondition();

    static {
        // 初始化10个独占资源
        for (int i = 1; i <= 10; i++) {
            freeResources.add(new Resource("Resource-" + i));
        }
    }

    public static Resource get(String testId) {
        lock.lock();
        try {
            logger.info("尝试为测试 {} 获取资源", testId);
            // 最多等待20分钟获取资源
            long timeout = 20 * 60 * 1000L;
            long startWait = System.currentTimeMillis();
            while (freeResources.isEmpty()) {
                long remainingTime = timeout - (System.currentTimeMillis() - startWait);
                if (remainingTime <= 0) {
                    throw new RuntimeException("测试 " + testId + " 等待20分钟仍无法获取可用资源");
                }
                // 释放锁等待,直到有资源被唤醒
                resourceAvailable.await(remainingTime);
            }

            // 获取第一个空闲资源
            Resource resource = freeResources.remove(0);
            // 绑定资源到当前测试
            resourcesInUse.computeIfAbsent(testId, k -> new CopyOnWriteArrayList<>()).add(resource);
            logger.info("测试 {} 成功获取资源: {}", testId, resource);
            return resource;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("获取资源时线程被中断", e);
        } finally {
            lock.unlock();
        }
    }

    public static boolean remove(String testId) {
        lock.lock();
        try {
            List<Resource> usedResources = resourcesInUse.remove(testId);
            if (usedResources != null && !usedResources.isEmpty()) {
                logger.info("释放测试 {} 使用的资源: {}", testId, usedResources);
                freeResources.addAll(usedResources);
                // 唤醒所有等待资源的线程
                resourceAvailable.notifyAll();
                return true;
            }
            return false;
        } finally {
            lock.unlock();
        }
    }

    public static boolean remove(ITestResult result) {
        String testId = result.getMethod().getTestClass().getName() + "." + result.getMethod().getMethodName();
        return remove(testId);
    }

    // 示例Resource类,根据实际业务调整
    public static class Resource {
        private final String id;

        public Resource(String id) {
            this.id = id;
        }

        @Override
        public String toString() {
            return id;
        }
    }
}

RemoveResource接口

import org.testng.ITestResult;
import org.testng.annotations.AfterMethod;

public interface RemoveResource {
    @AfterMethod
    default void removeResource(ITestResult result) {
        ResourceProvider.remove(result);
    }
}

使用方式

测试类实现RemoveResource接口,在测试方法中调用ResourceProvider.get()时,传入明确的测试标识,比如:

public class MyTest implements RemoveResource {
    @Test
    public void testCase1() {
        String testId = this.getClass().getName() + "." + new Object(){}.getClass().getEnclosingMethod().getName();
        Resource resource = ResourceProvider.get(testId);
        // 执行测试逻辑
    }
}

内容的提问来源于stack exchange,提问作者Keker

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 20:35:16