SheepDog1998

博客园 首页 新随笔 联系 订阅 管理

地震专题图批量生成系统:异步并发 + 细粒度锁 + 线程池监控实践全解析

 

一、业务背景与核心需求

 
本系统面向地震应急响应场景,核心目标是批量、高效、安全地生成多类型地震专题图(烈度图、震中距图、人口分布图、地质灾害图等),并支撑评估、损失计算、报告生成等上下游业务。
 

核心挑战

 
  1. 多类型专题图需并行生成,提升整体效率;
  2. SuperMap GIS 数据集操作存在线程安全问题,需避免并发读写错乱;
  3. 单张专题图生成失败不能影响整体流程,需有重试和异常隔离机制;
  4. 需精细化监控线程池状态,实时定位性能瓶颈、锁竞争、死锁等问题;
  5. 系统需 7×24 小时稳定运行,异常可快速排查。
 

二、核心技术架构与代码逻辑

 

2.1 整体流程设计
image

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组合任务。
 

解决方案:异常隔离 + 重试机制 + 监控告警

 
  1. 异常隔离:单个任务异常仅打印日志,不抛出CompletionException
  2. 自动重试:针对可恢复异常(License、空指针)重试 3 次;
  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:类型锁→数据集锁);
  • 未使用限时锁,导致线程无限等待。
 

解决方案:死锁检测 + 限时锁 + 锁顺序规范

 
  1. 死锁检测:通过ThreadMXBean实时检测死锁,打印死锁线程堆栈;
  2. 限时锁:所有锁操作使用tryLock,避免无限等待;
  3. 锁顺序规范:强制按 “数据集锁→类型锁” 的顺序获取,避免嵌套死锁。
// 死锁检测(线程池监控中已实现)
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);
  • 完成任务数增长缓慢。
 

解决方案:单个任务超时控制 + 监控驱动的任务调度

 
  1. 单个任务超时控制:设置 5 分钟超时,超时后取消任务;
  2. 动态任务调度:线程池负载高时,延迟执行非核心任务。
    // 单个任务超时控制
    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 线程池配置与监控

 
  1. 核心参数:核心线程数 = 6,最大线程数 = 18,队列容量 = 100,空闲时间 = 60 秒;
  2. 监控维度:基础信息、线程状态、死锁检测,每 10 秒输出结构化日志;
  3. 异常处理:死锁自动检测 + 堆栈打印,超时任务自动取消,拒绝策略使用CallerRunsPolicy
 

5.2 异步并发

 
  1. 任务拆分:按 “依赖关系” 拆分任务,并行执行无依赖任务;
  2. 异常隔离:单个任务异常不影响整体流程,使用whenComplete统一处理;
  3. 动态调度:基于线程池负载调整任务执行时机和锁超时时间。
 

5.3 并发控制

 
  1. 细粒度锁:按 “数据集名称” 拆分锁,避免全局锁;
  2. 限时锁:使用tryLock替代lock,动态调整超时时间(10-15 秒);
  3. 锁顺序:强制按 “数据集锁→类型锁” 获取,避免嵌套死锁。
 

5.4 容错机制

 
  1. 重试机制:针对可恢复异常(License、空指针)自动重试,动态调整重试间隔(3-5 秒);
  2. 资源释放:GIS 资源在 finally 块强制释放,避免内存泄漏;
  3. 超时控制:单个任务设置 5 分钟超时,避免线程池阻塞。
 

5.5 监控与调优

 
  1. 指标解读:关注 “增长率” 而非 “绝对值”,核心看活跃线程数、队列大小、线程状态分布;
  2. 调优原则:基于监控数据动态调整,而非静态配置;
  3. 日志规范:关键环节打印线程池状态,便于问题定位。
 

六、后续优化方向

 
  1. 监控可视化:将线程池监控数据接入 Prometheus+Grafana,实现可视化大屏;
  2. 动态配置:基于监控数据自动调整线程池参数(核心线程数、锁超时);
  3. 分布式锁:若部署多实例,将本地锁替换为 Redis 分布式锁,避免跨实例并发问题;
  4. 熔断降级:当某类专题图失败率过高时,暂停提交该类任务,避免线程池资源耗尽;
  5. 缓存优化:缓存高频使用的 SuperMap 工作空间,减少重复打开 / 关闭开销。
posted on 2026-01-28 09:09  SheepDog1998  阅读(18)  评论(0)    收藏  举报