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后合并(推荐)
这是多线程绘图的标准实践,避免共享绘图上下文:
- 给每个线程分配独立的
BufferedImage,使用其专属的Graphics对象; - 每个线程在自己的BufferedImage上完成绘制;
- 所有线程完成后,将分块图像合并到主绘图上下文。
修改后的线程初始化代码示例:
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
相关产品推荐
相关产品推荐

