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); } }
问题原因排查
- 同步锁下的sleep导致锁持有:
get()和remove()都是静态同步方法,本质上抢的是ResourceProvider类对象的锁。当资源耗尽时,get()里的线程会进入sleep(),但此时它还握着锁,remove()方法根本拿不到锁去释放资源,直接导致死锁——等待资源的线程占着锁sleep,释放资源的线程拿不到锁没法干活,资源永远无法释放。 - 测试名获取不可靠:用
Thread.currentThread().getStackTrace()[2]拿测试类和方法名的方式太依赖栈帧结构,不同JVM、不同调用场景下栈帧索引可能变化,导致测试名识别错误,资源绑定混乱,进一步加剧锁占用问题。 - 非线程安全集合隐患:
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
相关产品推荐
相关产品推荐

