You need to enable JavaScript to run this app. Win10+OpenJDK11环境下JPanel播放视频时drawImage偶发卡顿、画面暂停问题求助
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.11.06 03:54:04