Win10+OpenJDK11环境下JPanel播放视频时drawImage偶发卡顿、画面暂停问题求助
运行环境
- 操作系统:Windows 10 Pro
- Java版本:OpenJDK 11.0.7
现存问题
开发基于JPanel的视频播放程序时遇到两个核心卡顿问题:
Graphics.drawImage(Image, x, y, null)耗时波动极大:正常情况耗时低于5ms,异常时耗时超过50ms- 即便
drawImage本身耗时低于5ms,仍会偶发超过50ms的播放停顿,肉眼可清晰感知
核心实现代码
public class UI { private static final Logger log = LoggerFactory.getLogger(UI.class); ... private ReentrantReadWriteLock postVideoFrameReadWriteLock = new ReentrantReadWriteLock(); private ReentrantReadWriteLock.ReadLock postVideoFrameReadLock = postVideoFrameReadWriteLock.readLock(); private ReentrantReadWriteLock.WriteLock postVideoFrameWriteLock = postVideoFrameWriteLock.writeLock(); private BufferedImage _lcdPanelImage ; private JPanel lcdPanel; ... public UI() { ... lcdPanel = new JPanel() { @Override public void paintComponent(Graphics g) { // super.paintComponent(g); long timer0 = System.currentTimeMillis(), timer1=0, timer2=-1, timer3 = 0; postVideoFrameReadLock.lock(); if (_lcdPanelImage != null) { timer1 = System.currentTimeMillis(); g.drawImage(_lcdPanelImage, 0, 0, null); timer2 = System.currentTimeMillis(); } postVideoFrameReadLock.unlock(); timer3 = System.currentTimeMillis(); int diff = (int) (timer2-timer1); if (diff >5) { log.trace(" paint total cost:{} ms, drawImage cost: {} ms",(timer3- timer0), (timer2-timer1)); } } }; lcdPanel.setDoubleBuffered(true); ... } private void postOnLcdPanel(BufferedImage bufferedImage) { long timer0 = System.currentTimeMillis(); postVideoFrameWriteLock.lock(); _lcdPanelImage = bufferedImage; postVideoFrameWriteLock.unlock(); long timer1 = System.currentTimeMillis(); lcdPanel.repaint(); long timer2 = System.currentTimeMillis(); int diff1 = (int) (timer1 - timer0); int diff2 = (int) (timer2 - timer1); if (diff1 >0 || diff2 > 0) { log.trace(String.format("set and lock cost:%4d ms, repaint cost:%4d ms", diff1, diff2 )); } } @Singleton private class DisplayVideoOnLcdPanelWorker implements Runnable { private final Logger log = LoggerFactory.getLogger(DisplayVideoOnLcdPanelWorker.class); private void tsleep(long lsec) { try { Thread.sleep(lsec); } catch (InterruptedException e) { log.error(e.getMessage(), e); } } @Override public void run() { log.debug("DisplayVideoOnLcdPanelWorker starts ...."); boolean running = true; long timer0 = System.currentTimeMillis(), playVideoCount = 0; final int targetSleepms = 25; float fps = 0; while (running) { try { if (videoWaitPlayingQueue.size() == 0) { Thread.sleep(5); continue; } BufferedImage image = videoWaitPlayingQueue.poll(1, TimeUnit.MILLISECONDS); long timer1 = System.currentTimeMillis(); postOnLcdPanel(image); long timer3 = System.currentTimeMillis(); playVideoCount++; int playCost = (int) (timer3 - timer1); int diffSleep = targetSleepms -playCost; if (playCost > 10) { log.trace(String.format("Play cost:%4d, videoWaitPlayingQueue:%d", playCost, videoWaitPlayingQueue.size())); } tsleep(diffSleep > 3? diffSleep: 3); } catch (InterruptedException ex) { log.error(ex.getMessage(), ex); break; } } log.debug("DisplayVideoOnLcdPanelWorker stops ...."); } } }
运行日志样例
19:19:34.321 TRACE UI$DisplayVideoOnLcdPanelWorker: Play cost: 236, videoWaitPlayingQueue:200 19:19:40.858 TRACE UI: paint total cost:227 ms, drawImage cost: 227 ms 19:19:40.858 TRACE UI: set and lock cost: 1 ms, repaint cost: 0 ms 19:19:50.798 TRACE UI: paint total cost:182 ms, drawImage cost: 182 ms 19:19:52.596 TRACE UI: set and lock cost: 0 ms, repaint cost: 1 ms 19:19:52.710 TRACE UI: paint total cost:114 ms, drawImage cost: 114 ms 19:19:52.710 TRACE UI: set and lock cost: 2 ms, repaint cost: 0 ms 19:20:26.387 TRACE UI: paint total cost:101 ms, drawImage cost: 101 ms 19:20:28.086 TRACE UI: paint total cost:105 ms, drawImage cost: 105 ms 19:20:28.087 TRACE UI: set and lock cost: 3 ms, repaint cost: 0 ms 19:20:39.319 TRACE UI: set and lock cost: 0 ms, repaint cost: 1 ms 19:21:05.526 TRACE UI: paint total cost:284 ms, drawImage cost: 284 ms 19:21:07.556 TRACE UI: paint total cost:112 ms, drawImage cost: 112 ms 19:21:07.556 TRACE UI: set and lock cost: 3 ms, repaint cost: 0 ms 19:21:32.687 TRACE UI: paint total cost:129 ms, drawImage cost: 129 ms 19:21:32.687 TRACE UI: set and lock cost: 1 ms, repaint cost: 0 ms 19:21:37.217 TRACE UI: set and lock cost: 0 ms, repaint cost: 290 ms 19:21:37.217 TRACE UI$DisplayVideoOnLcdPanelWorker: Play cost: 290, videoWaitPlayingQueue:200 19:21:41.779 TRACE UI: set and lock cost: 0 ms, repaint cost: 302 ms
可落地的优化方向
drawImage性能波动的根因与优化
- 移除冗余锁逻辑:当前代码用
ReentrantReadWriteLock保护_lcdPanelImage的读写完全没必要。_lcdPanelImage是引用类型,赋值操作本身是原子的,只需要给变量加volatile修饰就能保证跨线程可见性。读写锁带来的锁竞争、上下文切换开销,是EDT绘制线程随机卡顿的重要来源,完全可以去掉。 - 统一图像色型消除格式转换开销:
drawImage耗时动辄上百ms的场景,90%以上是解码出的原始帧色型和系统原生色型不匹配,Swing绘制时会逐像素做格式转换。优化方式:所有解码完成的原始帧,在入队前先通过GraphicsConfiguration.createCompatibleImage()转成和当前屏幕显示格式完全一致的兼容图像,后续绘制时就不会触发耗时的逐像素转换,drawImage耗时会稳定在1-2ms。 - 开启硬件加速路径:OpenJDK 11在Windows上默认的硬件加速策略偏保守,可通过JVM启动参数
-Dsun.java2d.d3d=true开启Direct3D加速,让图像绘制走显卡管线,进一步降低CPU拷贝的开销。
偶发无理由停顿的根因与优化
- 避免触发
repaint()的同步重绘逻辑:从日志里能看到repaint()偶尔耗时290ms、302ms,这不是方法本身执行慢。Swing的RepaintManager平时只是把连续的repaint请求合并后丢给EDT事件队列,但如果检测到EDT阻塞、队列积压,就会直接在当前调用repaint的线程上同步执行绘制逻辑,跨线程的图像数据竞争、CPU缓存不命中会直接导致耗时暴涨。优化方式:所有UI更新操作通过SwingUtilities.invokeLater()提交到EDT执行,不要在视频解码线程直接调用repaint(),从根源避免同步重绘触发。 - 压缩EDT上的任务时长:哪怕
drawImage本身只要1ms,只要EDT被其他操作(比如窗口resize、组件布局计算、其他UI事件处理)阻塞,绘制任务就会在队列里排队,等待时间超过50ms就会出现肉眼可见的停顿。优化方式:所有耗时操作(帧解码、格式转换)全部挪到EDT之外的线程完成,EDT只负责把离屏准备好的图像快速blit到屏幕上,最大程度缩短单次EDT任务的执行时间。 - 避免帧队列积压:日志里频繁出现
videoWaitPlayingQueue:200的记录,说明解码速度已经超过实时播放速度,队列积压后遇到绘制卡顿,延迟会越拉越大。优化方式:给播放队列设置最大长度(一般2-3帧足够),当队列满时直接丢弃旧帧,只保留最新解码完成的帧提交给UI层,避免积压延迟传导到播放端。 - 修正帧调度的精度问题:当前用
Thread.sleep()做帧间隔控制的精度极低,Windows系统下默认的sleep最小精度只有15ms,加上系统线程抢占,很容易出现帧间隔随机波动。可以替换为基于纳秒计时器的自适应调度,固定匹配视频的帧率间隔(比如25帧对应40ms间隔),避免sleep带来的随机性延迟。
注意:
paintComponent里被注释掉的super.paintComponent(g)一定要恢复,否则组件不会自动清除背景,上一帧的残影会被直接叠在新帧上,不仅视觉效果差,还会额外增大绘制的计算量。
内容的提问来源于stack exchange,提问作者wureka
相关产品推荐
相关产品推荐

