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

Java多线程绘制SRTM3瓦片异常求助:单线程正常多线程图形混乱

多线程绘制Srtm3-Tile图形混乱问题排查

问题描述

单线程绘制Srtm3-Tile一切正常,但使用12核多线程时图形混乱,相关代码如下:

线程初始化代码(位于paint方法中)

int cpuCount = Runtime.getRuntime().availableProcessors(); //12 Cores
int blockSize = size / cpuCount;
BigMapPOJO bigMapPOJO = new BigMapPOJO(srtmBorderPOJO.getFileName(), bigMap);

SrtmV2Thread threadArray[] = new SrtmV2Thread[cpuCount];
//Init Threads, cpuCount=1 works fine...
for (int i = 0; i < cpuCount; i++) {
    SrtmV2Thread srtmV2Thread;
    if (i == cpuCount - 1) {
        srtmV2Thread = new SrtmV2Thread(g, map, bigMapPOJO, 0 + (i * blockSize), size, srtmBorderPOJO, min, max);
    } else {
        srtmV2Thread = new SrtmV2Thread(g, map, bigMapPOJO, 0 + (i * blockSize), blockSize + (i * blockSize), srtmBorderPOJO, min, max);
    }
    threadArray[i] = srtmV2Thread;
}
for (int i = 0; i < threadArray.length; i++) {
    //Start Threads
    threadArray[i].start();
}
for (int i = 0; i < threadArray.length; i++) {
    try {
        //Wait for completion
        threadArray[i].join();
    } catch (InterruptedException ex) {
        ex.printStackTrace();
    }
}

线程run方法

@Override
public void run() {

    GeoPosition geoPosition0 = new GeoPosition(srtmV2BorderPOJO.getCoord0().getLat(), srtmV2BorderPOJO.getCoord0().getLon());
    GeoPosition geoPosition1 = new GeoPosition(srtmV2BorderPOJO.getCoord1().getLat(), srtmV2BorderPOJO.getCoord1().getLon());
    GeoPosition geoPosition2 = new GeoPosition(srtmV2BorderPOJO.getCoord2().getLat(), srtmV2BorderPOJO.getCoord2().getLon());
    GeoPosition geoPosition3 = new GeoPosition(srtmV2BorderPOJO.getCoord3().getLat(), srtmV2BorderPOJO.getCoord3().getLon());

    Point2D pt0 = map.getTileFactory().geoToPixel(geoPosition0, map.getZoom());
    Point2D pt1 = map.getTileFactory().geoToPixel(geoPosition1, map.getZoom());
    Point2D pt2 = map.getTileFactory().geoToPixel(geoPosition2, map.getZoom());
    Point2D pt3 = map.getTileFactory().geoToPixel(geoPosition3, map.getZoom());

    /*
    0-1
    3-2
     */
    //calulate width and height of a single tile
    int resHor = (int) ((pt1.getX() - pt0.getX()));
    int resVer = (int) ((pt3.getY() - pt0.getY()));

    int horStep = (int) (size / (double) resHor);
    int verStep = (int) (size / (double) resVer);
    
    //start and end comes from the thread init
    for (int y = start; y < end; y += verStep) {

        for (int x = 0; x < size; x += horStep) {

            double ver = y / (double) (size);
            double hor = x / (double) (size);

            GeoPosition geoPosition = new GeoPosition(srtmV2BorderPOJO.getCoord0().getLat() - ver, srtmV2BorderPOJO.getCoord0().getLon() + hor);
            Point2D ptHeight = map.getTileFactory().geoToPixel(geoPosition, map.getZoom());

            short height = bigMapPOJO.getBigMap()[y][x];
            if (height < 0) {
                height = 0;
            }

            double percent = getPercentFromHeight(min, max, height);
            Color colorHeight = genColor(percent);

            double red = colorHeight.getRed();
            double green = colorHeight.getGreen();
            double blue = colorHeight.getBlue();
            
            //draw point in calculated color, according to height
            java.awt.Color color = new java.awt.Color((int) (red * 255), (int) (green * 255), (int) (blue * 255), 255);
            g.setColor(color);

            Point point = new Point((int) ptHeight.getX(), (int) ptHeight.getY());

            g.draw(new Rectangle(point));
        }
    }
}

问题原因

  • AWT Graphics对象非线程安全:所有线程共享同一个g(Graphics绘图上下文),多个线程同时调用g.setColor()和g.draw()会导致操作交叉——比如线程A设置颜色后,线程B立刻修改颜色,线程A执行绘制时用的是线程B的颜色,最终图形颜色与位置不匹配,出现混乱。
  • 无同步的共享资源操作:绘图动作没有同步机制,线程间绘制完全无序,叠加后导致图形错乱。

解决方案

方案1:给绘图操作加同步锁(仅用于验证问题)

在每个线程的绘图循环外层对g对象加锁,确保同一时间只有一个线程操作绘图上下文:

@Override
public void run() {
    // ... 前面的坐标计算代码不变

    synchronized(g) { // 加锁保护Graphics操作
        for (int y = start; y < end; y += verStep) {
            for (int x = 0; x < size; x += horStep) {
                // ... 原有绘图代码
            }
        }
    }
}

注意:这种方式会让多线程退化为串行执行,仅用于验证问题根源,无法发挥多线程性能优势。

方案2:分块绘制到独立BufferedImage后合并(推荐)

这是多线程绘图的标准实践,避免共享绘图上下文:

  1. 给每个线程分配独立的BufferedImage,使用其专属的Graphics对象;
  2. 每个线程在自己的BufferedImage上完成绘制;
  3. 所有线程完成后,将分块图像合并到主绘图上下文。

修改后的线程初始化代码示例:

int cpuCount = Runtime.getRuntime().availableProcessors();
int blockSize = size / cpuCount;
BigMapPOJO bigMapPOJO = new BigMapPOJO(srtmBorderPOJO.getFileName(), bigMap);

// 存储每个线程的绘制结果(图像+起始Y坐标)
List<ThreadDrawResult> threadResults = new ArrayList<>();
SrtmV2Thread threadArray[] = new SrtmV2Thread[cpuCount];

for (int i = 0; i < cpuCount; i++) {
    int start = i * blockSize;
    int end = (i == cpuCount - 1) ? size : start + blockSize;
    // 创建对应高度的独立图像
    BufferedImage threadImage = new BufferedImage(size, end - start, BufferedImage.TYPE_INT_ARGB);
    Graphics2D threadG = threadImage.createGraphics();
    threadResults.add(new ThreadDrawResult(threadImage, start));
    // 传递线程专属的Graphics对象
    threadArray[i] = new SrtmV2Thread(threadG, map, bigMapPOJO, start, end, srtmBorderPOJO, min, max);
}

// 启动并等待所有线程完成
for (SrtmV2Thread thread : threadArray) thread.start();
for (SrtmV2Thread thread : threadArray) {
    try {
        thread.join();
    } catch (InterruptedException ex) {
        ex.printStackTrace();
    }
}

// 合并所有分块图像到主Graphics
for (ThreadDrawResult result : threadResults) {
    g.drawImage(result.image, 0, result.startY, null);
}

// 辅助类:存储线程绘制结果
static class ThreadDrawResult {
    BufferedImage image;
    int startY;
    ThreadDrawResult(BufferedImage img, int y) {
        image = img;
        startY = y;
    }
}

方案3:使用ExecutorService管理线程(代码更简洁)

用Java并发工具类替代手动线程数组,简化线程管理:

int cpuCount = Runtime.getRuntime().availableProcessors();
int blockSize = size / cpuCount;
BigMapPOJO bigMapPOJO = new BigMapPOJO(srtmBorderPOJO.getFileName(), bigMap);

List<ThreadDrawResult> threadResults = new ArrayList<>();
ExecutorService executor = Executors.newFixedThreadPool(cpuCount);

for (int i = 0; i < cpuCount; i++) {
    int start = i * blockSize;
    int end = (i == cpuCount - 1) ? size : start + blockSize;
    BufferedImage threadImage = new BufferedImage(size, end - start, BufferedImage.TYPE_INT_ARGB);
    Graphics2D threadG = threadImage.createGraphics();
    ThreadDrawResult result = new ThreadDrawResult(threadImage, start);
    threadResults.add(result);
    
    // 提交任务到线程池
    executor.submit(() -> {
        // 这里是原run方法的代码,使用threadG绘图
        // ... 坐标计算、循环绘图逻辑
        threadG.dispose(); // 用完释放资源
    });
}

executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES); // 等待所有任务完成

// 合并图像
for (ThreadDrawResult result : threadResults) {
    g.drawImage(result.image, 0, result.startY, null);
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 07:17:05