地震专题图批量生成系统:异步并发 + 细粒度锁 + 线程池监控实践全解析
一、业务背景与核心需求
本系统面向地震应急响应场景,核心目标是批量、高效、安全地生成多类型地震专题图(烈度图、震中距图、人口分布图、地质灾害图等),并支撑评估、损失计算、报告生成等上下游业务。
核心挑战
- 多类型专题图需并行生成,提升整体效率;
- SuperMap GIS 数据集操作存在线程安全问题,需避免并发读写错乱;
- 单张专题图生成失败不能影响整体流程,需有重试和异常隔离机制;
- 需精细化监控线程池状态,实时定位性能瓶颈、锁竞争、死锁等问题;
- 系统需 7×24 小时稳定运行,异常可快速排查。
二、核心技术架构与代码逻辑
2.1 整体流程设计

2.2 核心技术栈
| 技术 / 组件 | 应用场景 | 核心作用 |
|---|---|---|
| CompletableFuture | 异步任务编排 | 实现多任务并行 / 串行依赖、异常隔离、超时控制 |
| ReentrantLock | 数据集并发控制 | 细粒度控制 GIS 数据集读写,避免并发错乱 |
| ThreadPoolTaskExecutor | 任务调度 | 隔离 IO/CPU 密集型任务,控制并发数 |
| ThreadMXBean | 线程池监控 | 实时监控线程状态、检测死锁、统计任务执行情况 |
| ScheduledExecutorService | 监控调度 | 定期执行线程池状态监控,输出结构化日志 |
| SuperMap iObjects Java SDK | GIS 出图 | 专题图绘制、数据集操作、布局导出 |
| 重试机制(Callable+AtomicInteger) | 容错处理 | 针对偶发异常(License / 空指针)自动重试 |
| 耗时统计 | 性能监控 | 定位各环节性能瓶颈 |
2.3 核心代码逻辑拆解
2.3.1 线程池配置与监控(核心基础)
该配置类是整个系统的并发基石,实现了线程池初始化 + 实时监控 + 死锁检测,关键逻辑:
package com.push.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Slf4j
@Configuration
@Component
public class ThreadPoolConfig {
@Value("${threads.coreSize}")
private Integer coreSize; // 核心线程数,建议配置为6
@Value("${threads.maxPoolSize}")
private Integer maxPoolSize; // 最大线程数,建议配置为18
@Value("${threads.queue}")
private Integer queue; // 队列容量,建议配置为100
@Value("${threads.keepTime}")
private Integer keepTime; // 线程空闲时间,建议配置为60
@Bean(name = "eqExecutor")
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 1. 核心参数配置
executor.setCorePoolSize(coreSize); // 核心线程数:6
executor.setMaxPoolSize(maxPoolSize); // 最大线程数:18
executor.setQueueCapacity(queue); // 队列容量:100
executor.setKeepAliveSeconds(keepTime); // 线程空闲时间:60秒
executor.setThreadNamePrefix("EqTask-"); // 线程名称前缀,便于监控
// 拒绝策略:调用者执行(避免任务丢失,适用于应急场景)
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
// 2. 定期监控线程池状态(每10秒执行一次,立即生效)
ScheduledExecutorService monitorExecutor = Executors.newSingleThreadScheduledExecutor();
monitorExecutor.scheduleAtFixedRate(() -> {
ThreadPoolExecutor threadPoolExecutor = executor.getThreadPoolExecutor();
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 2.1 基础信息统计
int activeCount = executor.getActiveCount(); // 活跃线程数
int corePoolSize = executor.getCorePoolSize(); // 核心线程数
int maxPoolSize = executor.getMaxPoolSize(); // 最大线程数
int queueSize = threadPoolExecutor.getQueue().size(); // 队列大小
long completedTaskCount = threadPoolExecutor.getCompletedTaskCount(); // 累计完成任务数
// 2.2 线程状态统计(按状态分类)
Map<Thread.State, Integer> threadStateMap = new HashMap<>();
ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(threadMXBean.getAllThreadIds());
for (ThreadInfo threadInfo : threadInfos) {
if (threadInfo != null && threadInfo.getThreadName().startsWith("EqTask-")) {
Thread.State state = threadInfo.getThreadState();
threadStateMap.put(state, threadStateMap.getOrDefault(state, 0) + 1);
}
}
// 2.3 死锁检测(核心:避免系统卡死)
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
boolean hasDeadlock = deadlockedThreads != null && deadlockedThreads.length > 0;
// 2.4 打印结构化监控日志
log.info("线程池状态:" +
"\n- 基础信息:活跃线程数={}, 核心线程数={}, 最大线程数={}, 队列大小={}, 完成任务数={}" +
"\n- 线程状态:{}" +
"\n- 死锁检测:{}",
activeCount, corePoolSize, maxPoolSize, queueSize, completedTaskCount,
threadStateMap, hasDeadlock ? "存在死锁!" : "无死锁");
// 2.5 死锁详情打印(便于定位问题)
if (hasDeadlock) {
log.error("死锁线程详情:");
for (long threadId : deadlockedThreads) {
ThreadInfo threadInfo = threadMXBean.getThreadInfo(threadId);
log.error("线程 ID:{},名称:{},状态:{},堆栈:{}",
threadId, threadInfo.getThreadName(), threadInfo.getThreadState(),
Arrays.toString(threadInfo.getStackTrace()));
}
}
}, 0, 10, TimeUnit.SECONDS); // 初始延迟0秒,每10秒执行一次
return executor;
}
}
2.3.2 入口方法:整体流程编排(wsInfluenceField)
该方法是整个流程的入口,核心职责是任务拆解与依赖编排,并结合线程池监控数据优化任务调度:
public void wsInfluenceField(Long id, String eqType, String fieldGroupId) {
// 初始化耗时统计(线程安全)
ConcurrentHashMap<String, Long> timeStats = new ConcurrentHashMap<>();
long totalStartTime = System.currentTimeMillis();
log.info("【耗时统计】开始==================================================================");
log.info("【耗时统计】流程开始,地震ID: {}, 类型: {}, 开始时间: {}",
id, eqType, DateTimeUtil.formatDate(new Date(totalStartTime), "yyyy-MM-dd HH:mm:ss.SSS"));
// 1. 基础数据准备(同步):影响场计算、参数查询、数据入库
long step1Start = System.currentTimeMillis();
RealAnalogEQDto realAnalogEQDto = translateObjectToVo(id, eqType);
List<InfluenceFieldResult> fieldResults = getFieldResult(realAnalogEQDto, influenceParam, fieldGroupId);
// 影响场数据入库(按烈度降序排序)
for (InfluenceFieldResult field : sortedList) {
field.setGeom(ModelFormula.getOffSetGeom(field, ScaleParamType.BOTH, l));
fieldResultMapper.insert(field);
}
timeStats.put("1. 生成影响场逻辑", System.currentTimeMillis() - step1Start);
log.info("【耗时统计】1. 生成影响场逻辑耗时: {} ms", timeStats.get("1. 生成影响场逻辑"));
// 2. IO密集型任务并行执行:更新烈度表、面积/人口数据(使用监控的线程池)
long step2Start = System.currentTimeMillis();
CompletableFuture<Void> updateLieDuFuture = CompletableFuture.runAsync(
() -> updateLieDuAboutEqTable(id, eqType, fieldResults.get(0).getLiedu()),
eqExecutor // 注入配置好的监控线程池
);
CompletableFuture<Void> updateAreaFuture = CompletableFuture.runAsync(
() -> updateFiledAreaAndPopulation(realAnalogEQDto, group.getFieldGroupId()),
eqExecutor
);
CompletableFuture<Void> stage1Done = CompletableFuture.allOf(updateAreaFuture, updateLieDuFuture);
timeStats.put("2. 并行执行基础数据更新", System.currentTimeMillis() - step2Start);
log.info("【耗时统计】2. 并行执行基础数据更新耗时: {} ms", timeStats.get("2. 并行执行基础数据更新"));
// 3. 评估任务(依赖基础数据更新完成)
long step3Start = System.currentTimeMillis();
String resultGroupId = needAssessment ? UUID.randomUUID().toString() : "";
CompletableFuture<AssessmentResultVo> assessmentFuture = stage1Done.thenApplyAsync(v -> {
return assementService.createYxcAssessment(id, eqType, fieldGroupId, resultGroupId);
}, eqExecutor);
timeStats.put("3. 评估任务初始化", System.currentTimeMillis() - step3Start);
log.info("【耗时统计】3. 评估任务初始化耗时: {} ms", timeStats.get("3. 评估任务初始化"));
// 4. 多波次专题图生成(依赖评估完成,结合线程池监控动态调整)
assessmentFuture.thenRunAsync(() -> {
// 第一波专题图(无依赖)
CompletableFuture<Void> t1Future = CompletableFuture.runAsync(() -> {
log.info("【第一波专题图任务】开始执行(线程池活跃数:{})", eqExecutor.getActiveCount());
long t1Start = System.currentTimeMillis();
List<String> t1List = thematicMap.get(1);
if (t1List != null && !t1List.isEmpty()) {
try {
thematicMapService.makeThematicMapByEQId(id, eqType, t1List).join();
} catch (Exception e) {
log.error("生成第一波专题图失败", e);
}
}
long t1Time = System.currentTimeMillis() - t1Start;
timeStats.put("5. 第一波专题图", t1Time);
log.info("【第一波专题图任务】耗时: {} ms(线程池队列大小:{})", t1Time, eqExecutor.getThreadPoolExecutor().getQueue().size());
}, eqExecutor);
// 第二/三/四波专题图(并行,基于线程池状态动态调度)
CompletableFuture<Void> t2Future = CompletableFuture.runAsync(() -> {
// 若线程池活跃数超过80%,延迟执行(避免过载)
if (eqExecutor.getActiveCount() > eqExecutor.getCorePoolSize() * 0.8) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
List<String> t2List = thematicMap.get(2);
if (t2List != null && !t2List.isEmpty()) {
thematicMapService.makeThematicMapByEQId(id, eqType, t2List).join();
}
}, eqExecutor);
// 专报生成(依赖第二波专题图)
t2Future.thenRunAsync(() -> {
if (eqLevel >= 3.0) {
String pdfPath = reportService.generate(1, "专报", id, eqType, fieldGroupId, null, resultVo);
}
}, eqExecutor);
// 所有任务完成后汇总耗时(结合线程池监控数据)
CompletableFuture.allOf(t1Future, t2Future, t3Future, t4Future)
.whenComplete((v, e) -> {
long totalTime = System.currentTimeMillis() - totalStartTime;
log.info("【耗时统计】总用时: {} ms | 线程池完成任务数: {}",
totalTime, eqExecutor.getThreadPoolExecutor().getCompletedTaskCount());
});
}, eqExecutor);
}
2.3.3 专题图批量生成(makeThematicMapByEQId)
核心职责:为每个专题图类型创建异步任务,结合线程池监控实现异常隔离和超时控制。
public CompletableFuture<Void> makeThematicMapByEQId(Long id, String eqType, List<String> thematicTypes) throws Exception {
if (thematicTypes == null || thematicTypes.isEmpty()) {
log.warn("专题图类型列表为空,地震ID: {}", id);
return CompletableFuture.completedFuture(null);
}
// 1. 准备共享数据(影响场、地震信息)
RealAnalogEQDto realAnalogEQDto = translateObjectToVo(id, eqType);
List<InfluenceFieldResult> fieldResults = fieldResultMapper.selectList(queryWrapper);
// 2. 为每个专题图类型创建异步任务
List<CompletableFuture<Void>> futures = new ArrayList<>();
for (String thematicType : thematicTypes) {
String finalThematicType = thematicType; // 捕获循环变量,避免闭包陷阱
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
log.info("开始处理专题图 | eqId={} | 类型={} | 线程池活跃数={}",
id, finalThematicType, eqExecutor.getActiveCount());
processThematicMap(id, eqType, finalThematicType, realAnalogEQDto, fieldResults);
log.info("专题图处理成功 | eqId={} | 类型={}", id, finalThematicType);
} catch (Exception e) {
// 单个任务异常仅打印日志,不中断整体流程
log.error("单张专题图最终失败(重试耗尽) | eqId={} | 类型={}", id, finalThematicType, e);
}
}, eqExecutor);
// 3. 单个任务超时控制(5分钟),避免占用线程池资源
try {
future.get(5, TimeUnit.MINUTES);
} catch (TimeoutException e) {
log.error("专题图生成超时 | eqId={} | 类型={} | 线程池队列大小={}",
id, finalThematicType, eqExecutor.getThreadPoolExecutor().getQueue().size());
future.cancel(true); // 取消超时任务,释放线程
}
futures.add(future);
}
// 4. 组合所有任务,返回整体Future(无论单个任务成败,整体都完成)
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.whenComplete((v, e) -> {
if (e == null) {
log.info("所有专题图任务执行完成(部分可能失败) | eqId={} | 线程池完成任务数={}",
id, eqExecutor.getThreadPoolExecutor().getCompletedTaskCount());
} else {
log.error("专题图批量任务异常(整体流程) | eqId={}", id, e);
}
});
}
2.3.4 单张专题图生成(processThematicMap)
核心职责:专题图类型路由 + 细粒度锁控制 + 重试机制,结合线程池监控优化锁竞争:
private void processThematicMap(Long id, String eqType, String thematicType,
RealAnalogEQDto realAnalogEQDto, List<InfluenceFieldResult> fieldResults) throws Exception {
// 1. 专题图类型锁:同一类型专题图串行生成(避免重复出图)
Object lock = thematicTypeLocks.computeIfAbsent(thematicType, k -> new Object());
// 2. 构建出图参数
SuperMapWorkspaceParam mapWorkspaceParams = mapWorkspaceMapper.selectOne(wrapper);
String relativePath = buildRelativePath(realAnalogEQDto.getFilePath(), cacheValue, thematicName);
mapWorkspaceParams.setLayoutPath(relativePath);
// 3. 加锁执行出图逻辑(同一类型串行),打印锁竞争日志
log.info("准备获取专题图类型锁 | eqId={} | 类型={} | 线程池状态={}",
id, thematicType, eqExecutor.getActiveCount() + "/" + eqExecutor.getCorePoolSize());
synchronized (lock) {
// 按专题图类型路由,带重试机制(最大3次)
if (ThematicEnum.EQ_Distance.getTableKey().equals(thematicType)) {
retryThematicTask(() -> {
makeDistanceMap(realAnalogEQDto, mapWorkspaceParams, fieldResults);
return null;
}, 3, id, thematicType);
} else if (ThematicEnum.EQ_Intensity.getTableKey().equals(thematicType)) {
retryThematicTask(() -> {
doMapIntensity(fieldResults, mapWorkspaceParams, realAnalogEQDto);
return null;
}, 3, id, thematicType);
} else {
retryThematicTask(() -> {
doMap(fieldResults, mapWorkspaceParams, realAnalogEQDto);
return null;
}, 3, id, thematicType);
}
}
log.info("释放专题图类型锁 | eqId={} | 类型={}", id, thematicType);
// 4. 保存出图记录
saveReportRecord(id, thematicType, mapWorkspaceParams, cacheValue, relativePath, thematicName);
}
2.3.5 GIS 出图核心逻辑(doMap)
核心职责:SuperMap 数据集操作 + 细粒度锁控制 + 资源释放,结合线程池监控优化锁超时:
public void doMap(List<InfluenceFieldResult> fieldResult, SuperMapWorkspaceParam param, RealAnalogEQDto eqDto) throws Exception {
// 资源声明(需释放的GIS资源)
Workspace workspace = null;
DatasetVector datasetVector_Plane = null;
Recordset recordset_Plane = null;
MapLayout mapLayout = null;
try {
// 1. 打开SuperMap工作空间
workspace = new Workspace();
WorkspaceConnectionInfo workspaceConnectionInfo = new WorkspaceConnectionInfo();
workspaceConnectionInfo.setType(WorkspaceType.SMWU);
workspaceConnectionInfo.setServer(jrePath + param.getWorkSpacePath());
if (!workspace.open(workspaceConnectionInfo)) {
throw new Exception("工作空间打开失败:" + param.getWorkSpacePath());
}
Datasource datasource = workspace.getDatasources().get(param.getDatasource());
// 2. 面数据集操作(影响场):细粒度锁控制(结合线程池监控调整超时)
if (param.getDatasetVector() != null) {
datasetVector_Plane = (DatasetVector) datasource.getDatasets().get(param.getDatasetVector());
// 按数据集名称获取独立锁(避免全局锁)
ReentrantLock datasetLock = datasetLockMap.computeIfAbsent(param.getDatasetVector(), k -> new ReentrantLock());
// 动态调整锁超时:线程池负载高时,延长超时时间
long lockTimeout = eqExecutor.getActiveCount() > eqExecutor.getCorePoolSize() * 0.8 ? 15 : 10;
log.info("准备获取数据集锁 | 类型={} | 超时={}秒 | 线程池活跃数={}",
param.getWorkSpaceKey(), lockTimeout, eqExecutor.getActiveCount());
// 限时获取锁,避免死锁
if (datasetLock.tryLock(lockTimeout, TimeUnit.SECONDS)) {
try {
recordset_Plane = datasetVector_Plane.getRecordset(false, CursorType.DYNAMIC);
// 清空数据集(避免历史数据干扰)
recordset_Plane.moveFirst();
recordset_Plane.deleteAll();
recordset_Plane.update();
// 写入影响场数据
addNewRing(fieldResult, recordset_Plane, param);
} finally {
datasetLock.unlock(); // 确保锁释放
log.info("释放数据集锁 | 类型={}", param.getWorkSpaceKey());
}
} else {
log.error("获取面数据集锁超时 | 类型={} | 线程池状态={}",
param.getWorkSpaceKey(), eqExecutor.getActiveCount() + "/" + eqExecutor.getMaxPoolSize());
throw new RuntimeException("获取面数据集锁超时 | 类型=" + param.getWorkSpaceKey());
}
}
// 3. 点数据集操作(震中位置):同理使用细粒度锁
DatasetVector datasetVector_Point = (DatasetVector) datasource.getDatasets().get(param.getVectorTwo());
ReentrantLock vectorLock = datasetLockMap.computeIfAbsent(param.getVectorTwo(), k -> new ReentrantLock());
long pointLockTimeout = eqExecutor.getActiveCount() > eqExecutor.getCorePoolSize() * 0.8 ? 15 : 10;
if (vectorLock.tryLock(pointLockTimeout, TimeUnit.SECONDS)) {
try {
Recordset recordset_Point = datasetVector_Point.getRecordset(false, CursorType.DYNAMIC);
recordset_Point.deleteAll();
recordset_Point.update();
// 写入震中坐标
double[][] coords = {{Double.parseDouble(eqDto.getLon()), Double.parseDouble(eqDto.getLat())}};
addPointNew(coords, recordset_Point, eqDto.getDetail());
} finally {
vectorLock.unlock();
}
}
// 4. 地图布局设置(比例尺、中心点、表格数据)
GeoMap geoMap = new GeoMap();
geoMap.setMapCenter(new Point2D(Double.parseDouble(eqDto.getLon()), Double.parseDouble(eqDto.getLat())));
geoMap.setMapScale(scale); // 按专题图类型动态设置比例尺
// 表格数据填充(人口、水库、医院等)
if (Objects.equals(param.getTableListNum(), Constants.TWO)) {
if (ThematicEnum.EQ_Population_Grille.getTableKey().equals(param.getWorkSpaceKey())) {
statisticsService.saveTwoDataRk(eqId, fieldGroupId); // 人口统计数据
}
}
// 5. 导出专题图为JPG
MapLayoutControl mapLayoutControl = new MapLayoutControl();
mapLayout = mapLayoutControl.getMapLayout();
mapLayout.setWorkspace(workspace);
mapLayout.open(param.getLayoutName());
String mapImagePath = jrePath + param.getLayoutPath();
mapLayout.outputLayoutToJPG(mapImagePath, param.getJpgDpi(), param.getJpgCompress());
// 6. 出图后清空数据集
if (recordset_Plane != null) {
recordset_Plane.deleteAll();
recordset_Plane.update();
}
} catch (Exception e) {
log.error("出图失败 | eqId={} | 类型={} | 线程池状态={}",
eqDto.getId(), param.getWorkSpaceKey(), eqExecutor.getActiveCount() + "/" + eqExecutor.getMaxPoolSize(), e);
throw e;
} finally {
// 7. 资源释放(核心:finally块+空值校验+异常捕获)
if (recordset_Plane != null) {
try { recordset_Plane.close(); recordset_Plane.dispose(); } catch (Exception e) { log.warn("关闭面记录集失败", e); }
}
if (mapLayout != null) {
try { mapLayout.close(); } catch (Exception e) { log.warn("关闭布局失败", e); }
}
if (workspace != null) {
try { workspace.close(); } catch (Exception e) { log.warn("关闭工作空间失败", e); }
}
}
}
2.3.6 重试机制(retryThematicTask)
核心职责:针对偶发异常自动重试,结合线程池监控调整重试策略:
private void retryThematicTask(Callable<Void> retryTask, int maxRetry, Long id, String type) throws Exception {
AtomicInteger retryCount = new AtomicInteger(0);
while (retryCount.get() < maxRetry) {
try {
retryTask.call(); // 执行出图任务
return; // 成功则退出
} catch (Exception e) {
retryCount.incrementAndGet();
// 判断是否为可恢复异常(License、空指针、数据集相关)
if (isRecoverableException(e) && retryCount.get() < maxRetry) {
// 动态调整重试间隔:线程池负载高时,延长间隔
long retryInterval = eqExecutor.getActiveCount() > eqExecutor.getCorePoolSize() * 0.8 ? 5000 : 3000;
log.warn("专题图重试 | 第{}次 | eqId={} | 类型={} | 重试间隔={}ms | 线程池活跃数={}",
retryCount.get(), id, type, retryInterval, eqExecutor.getActiveCount());
Thread.sleep(retryInterval); // 重试间隔
} else {
log.error("专题图失败 | 重试{}次后仍失败 | eqId={} | 类型={} | 线程池完成任务数={}",
retryCount.get(), id, type, eqExecutor.getThreadPoolExecutor().getCompletedTaskCount(), e);
throw e; // 重试耗尽抛出异常
}
}
}
}
// 判断可恢复异常
private boolean isRecoverableException(Throwable e) {
if (e == null) return false;
// 1. 空指针(SuperMap License/数据集偶发空)
if (e instanceof NullPointerException) {
for (StackTraceElement stack : e.getStackTrace()) {
if (stack.getClassName().startsWith("com.supermap") || stack.getClassName().contains("Dataset")) {
return true;
}
}
}
// 2. License相关异常
if (e.getMessage() != null && (e.getMessage().contains("License") || e.getMessage().contains("许可"))) {
return true;
}
// 3. 递归检查根因
return isRecoverableException(e.getCause());
}
三、核心问题排查与解决方案(结合监控)
3.1 问题 1:多线程并发操作 GIS 数据集导致数据错乱
现象(从监控日志发现)
- 线程池监控显示
TIMED_WAITING线程数长期偏高(如 5/6); - 专题图出现重复数据 / 缺失数据;
- SuperMap 抛出 “数据集被占用”“记录集已关闭” 等异常。
根因
- 多个线程同时读写同一 SuperMap 数据集,无并发控制;
- 全局锁导致所有专题图串行生成,效率极低。
解决方案:细粒度锁(按数据集名称)+ 监控驱动的锁超时调整
// 动态调整锁超时:线程池负载高时,延长超时时间
long lockTimeout = eqExecutor.getActiveCount() > eqExecutor.getCorePoolSize() * 0.8 ? 15 : 10;
if (datasetLock.tryLock(lockTimeout, TimeUnit.SECONDS)) {
try {
// 数据集操作逻辑
} finally {
datasetLock.unlock(); // 确保释放
}
} else {
throw new RuntimeException("获取锁超时 | 线程池活跃数=" + eqExecutor.getActiveCount());
}
监控指标验证
- 优化前:
TIMED_WAITING=5, RUNNABLE=1; - 优化后:
TIMED_WAITING=2, RUNNABLE=4,锁竞争明显缓解。
3.2 问题 2:单张专题图失败导致整体流程中断
现象(从监控日志发现)
- 线程池完成任务数增长停滞;
- 单个任务异常导致
CompletionException,中断allOf组合任务。
解决方案:异常隔离 + 重试机制 + 监控告警
- 异常隔离:单个任务异常仅打印日志,不抛出
CompletionException; - 自动重试:针对可恢复异常(License、空指针)重试 3 次;
- 监控告警:重试失败时,打印线程池状态,便于定位问题。
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
processThematicMap(id, eqType, finalThematicType, realAnalogEQDto, fieldResults);
} catch (Exception e) {
// 打印线程池状态,辅助排查
log.error("单张专题图失败 | eqId={} | 类型={} | 线程池状态={}",
id, finalThematicType, eqExecutor.getActiveCount() + "/" + eqExecutor.getMaxPoolSize(), e);
}
}, eqExecutor);
3.3 问题 3:线程池死锁导致系统卡死
现象(从监控日志发现)
- 线程池监控日志显示 “存在死锁!”;
- 活跃线程数长期等于核心线程数,队列大小持续增长;
- 专题图生成任务完全停滞。
根因
- 锁嵌套顺序不一致(如线程 1:数据集锁→类型锁;线程 2:类型锁→数据集锁);
- 未使用限时锁,导致线程无限等待。
解决方案:死锁检测 + 限时锁 + 锁顺序规范
- 死锁检测:通过
ThreadMXBean实时检测死锁,打印死锁线程堆栈; - 限时锁:所有锁操作使用
tryLock,避免无限等待; - 锁顺序规范:强制按 “数据集锁→类型锁” 的顺序获取,避免嵌套死锁。
// 死锁检测(线程池监控中已实现)
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
boolean hasDeadlock = deadlockedThreads != null && deadlockedThreads.length > 0;
if (hasDeadlock) {
log.error("死锁线程详情:");
for (long threadId : deadlockedThreads) {
ThreadInfo threadInfo = threadMXBean.getThreadInfo(threadId);
log.error("线程 ID:{},名称:{},状态:{},堆栈:{}",
threadId, threadInfo.getThreadName(), threadInfo.getThreadState(),
Arrays.toString(threadInfo.getStackTrace()));
}
}
3.4 问题 4:任务超时导致线程池阻塞
现象(从监控日志发现)
- 线程池活跃线程数长期占满核心线程数(6/6);
- 队列大小持续增长(如 50/100);
- 完成任务数增长缓慢。
解决方案:单个任务超时控制 + 监控驱动的任务调度
- 单个任务超时控制:设置 5 分钟超时,超时后取消任务;
- 动态任务调度:线程池负载高时,延迟执行非核心任务。
// 单个任务超时控制 try { future.get(5, TimeUnit.MINUTES); } catch (TimeoutException e) { log.error("专题图生成超时 | eqId={} | 类型={} | 队列大小={}", id, finalThematicType, eqExecutor.getThreadPoolExecutor().getQueue().size()); future.cancel(true); } // 动态任务调度 if (eqExecutor.getActiveCount() > eqExecutor.getCorePoolSize() * 0.8) { Thread.sleep(1000); // 延迟1秒执行,降低线程池负载 }
四、性能监控与调优(核心)
4.1 线程池监控核心指标解读
| 指标 | 正常范围 | 异常阈值 | 优化方向 |
|---|---|---|---|
| 活跃线程数 | 核心线程数 ×0.2 ~ 0.8 | > 核心线程数 ×0.8 | 1. 增加核心线程数;2. 优化任务耗时;3. 延迟非核心任务 |
| 队列大小 | 0 ~ 队列容量 ×0.2 | > 队列容量 ×0.5 | 1. 增加队列容量;2. 优化任务耗时;3. 增加最大线程数 |
| 线程状态(RUNNABLE) | > 核心线程数 ×0.5 | < 核心线程数 ×0.2 | 1. 检查锁竞争;2. 优化 IO 操作;3. 调整锁超时 |
| 线程状态(TIMED_WAITING) | < 核心线程数 ×0.5 | > 核心线程数 ×0.8 | 1. 优化锁粒度;2. 调整锁超时;3. 检查死锁 |
| 完成任务数增长率 | >10 个 / 10 秒 | <1 个 / 10 秒 | 1. 优化单任务耗时;2. 增加并发数;3. 排查异常 |
4.2 调优实践(基于监控数据)
第一步:初始配置
- 核心线程数 = 3,最大线程数 = 9,队列容量 = 50;
- 监控发现:活跃线程数 = 3,队列大小 = 30,TIMED_WAITING=2 → 负载过高。
第二步:第一次调优
- 核心线程数 = 6,最大线程数 = 18,队列容量 = 100;
- 监控发现:活跃线程数 = 4,队列大小 = 5,TIMED_WAITING=2 → 负载适中。
第三步:第二次调优(结合锁竞争)
- 锁超时从 5 秒→10 秒(负载高时 15 秒);
- 重试间隔从 3 秒→5 秒(负载高时);
- 监控发现:TIMED_WAITING=1,RUNNABLE=5 → 锁竞争缓解。
4.3 耗时统计 + 监控联动
通过
ConcurrentHashMap记录各环节耗时,结合线程池监控数据定位瓶颈:// 所有任务完成后打印汇总
CompletableFuture.allOf(t1Future, t2Future, t3Future, t4Future)
.whenComplete((v, e) -> {
long totalTime = System.currentTimeMillis() - totalStartTime;
log.info("【耗时统计】总用时: {} ms | 线程池完成任务数: {} | 活跃线程数: {}",
totalTime, eqExecutor.getThreadPoolExecutor().getCompletedTaskCount(), eqExecutor.getActiveCount());
// 按顺序打印各环节耗时
String[] orderedSteps = {
"1. 生成影响场逻辑", "2. 并行执行基础数据更新", "5. 第一波专题图", "10. 第二波专题图"
};
for (String step : orderedSteps) {
Long time = timeStats.get(step);
if (time != null) {
log.info("【耗时统计】{}: {} ms", step, time);
}
}
});
五、最佳实践总结
5.1 线程池配置与监控
- 核心参数:核心线程数 = 6,最大线程数 = 18,队列容量 = 100,空闲时间 = 60 秒;
- 监控维度:基础信息、线程状态、死锁检测,每 10 秒输出结构化日志;
- 异常处理:死锁自动检测 + 堆栈打印,超时任务自动取消,拒绝策略使用
CallerRunsPolicy。
5.2 异步并发
- 任务拆分:按 “依赖关系” 拆分任务,并行执行无依赖任务;
- 异常隔离:单个任务异常不影响整体流程,使用
whenComplete统一处理; - 动态调度:基于线程池负载调整任务执行时机和锁超时时间。
5.3 并发控制
- 细粒度锁:按 “数据集名称” 拆分锁,避免全局锁;
- 限时锁:使用
tryLock替代lock,动态调整超时时间(10-15 秒); - 锁顺序:强制按 “数据集锁→类型锁” 获取,避免嵌套死锁。
5.4 容错机制
- 重试机制:针对可恢复异常(License、空指针)自动重试,动态调整重试间隔(3-5 秒);
- 资源释放:GIS 资源在 finally 块强制释放,避免内存泄漏;
- 超时控制:单个任务设置 5 分钟超时,避免线程池阻塞。
5.5 监控与调优
- 指标解读:关注 “增长率” 而非 “绝对值”,核心看活跃线程数、队列大小、线程状态分布;
- 调优原则:基于监控数据动态调整,而非静态配置;
- 日志规范:关键环节打印线程池状态,便于问题定位。
六、后续优化方向
- 监控可视化:将线程池监控数据接入 Prometheus+Grafana,实现可视化大屏;
- 动态配置:基于监控数据自动调整线程池参数(核心线程数、锁超时);
- 分布式锁:若部署多实例,将本地锁替换为 Redis 分布式锁,避免跨实例并发问题;
- 熔断降级:当某类专题图失败率过高时,暂停提交该类任务,避免线程池资源耗尽;
- 缓存优化:缓存高频使用的 SuperMap 工作空间,减少重复打开 / 关闭开销。
浙公网安备 33010602011771号