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

Java FlatLaf UI可见时非EDT线程序列化TableModel抛空指针异常

问题说明

在使用FlatLaf外观的Swing程序中,当关联的JTable组件已在JFrame中处于可见状态时,尝试在非EDT线程序列化TableModel对象会稳定抛出NullPointerException,仅在EDT线程执行序列化操作可正常运行。
空指针异常直接触发信息:

Cannot invoke "javax.swing.CellRendererPane.paintComponent(java.awt.Graphics, java.awt.Component, java.awt.Container, int, int, int, int, boolean)" because "this.rendererPane" is null

本次复现使用的运行环境为AdoptOpenJDK14(14.0.2.12-hotspot),经测试验证:主线程、自定义子线程执行序列化均会触发异常,仅EDT线程执行序列化可正常运行。

根本原因

Swing采用严格的单线程模型:所有Swing组件、UI相关对象的访问、修改必须在EDT(事件分发线程)执行。
该异常的本质是:序列化AbstractTableModel时,Java默认序列化机制会遍历整个对象引用链,间接触达关联的JTable、BasicTableUI、FlatLaf自定义UI实例。这些UI类的内部字段(如rendererPane、绑定的table实例)仅在EDT执行UI初始化、绘制流程时才会完成赋值,非EDT线程访问时这些字段处于未初始化/状态不一致的情况,直接触发空指针。
注意:该问题不是FlatLaf独有bug,哪怕使用原生LookAndFeel,非EDT访问Swing对象本身就是违规操作,只是FlatLaf的UI实现让这个竞态条件100%触发而已。

最小可复现代码

程序入口为Serialization.java,代码如下:

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.swing.JFrame;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.table.AbstractTableModel;

import com.formdev.flatlaf.FlatDarkLaf;

public class Serialization {

    enum SERIALIZATION_THREAD {
        MAIN,
        THREAD,
        EDT
    }

    public static void main(String[] args) throws Exception {

        // 创建带frame和model的GUI
        UI ui = new UI();

        // 序列化table model
        System.out.println("SERIALIZING ON MAIN THREAD");
        sleep(1000);
        serialize(ui.model, SERIALIZATION_THREAD.MAIN); // 运行失败

        System.out.println("SERIALIZING ON SEPARATE THREAD");
        sleep(1000);
        serialize(ui.model, SERIALIZATION_THREAD.THREAD); // 运行失败

        System.out.println("SERIALIZING ON EDT");
        sleep(1000);
        serialize(ui.model, SERIALIZATION_THREAD.EDT); // 运行正常
    }

    public static void serialize(Serializable object, SERIALIZATION_THREAD thread) {
        switch (thread) {
            case MAIN:
                serialize0(object);
                break;
            case THREAD:
                new Thread(() -> serialize0(object)).start();
                break;
            case EDT:
                SwingUtilities.invokeLater(() -> serialize0(object));
                break;
            default:
                break;
        }
    }

    public static void serialize0(Serializable object) {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("some.dat"))) {
            oos.writeObject(object);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void sleep(int ms){
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class UI {

    JFrame frame;
    MyAbstractModel model;

    public UI() throws InvocationTargetException, InterruptedException {

        SwingUtilities.invokeAndWait(() -> {
            FlatDarkLaf.setup(); // 使用FlatLaf时序列化触发空指针

            model = new MyAbstractModel();
            frame = new JFrame();
            frame.add(new JTable(model));
            frame.setSize(300, 300);
            frame.setLocationRelativeTo(null);
            frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
            frame.setVisible(true);

            System.out.println("Done Building GUI");

        });
    }
}

class MyAbstractModel extends AbstractTableModel {

    private List<String> list = new ArrayList<>();

    public MyAbstractModel() {
        list = Collections.synchronizedList(new ArrayList<>());
        list.add("A");
        list.add("B");
        list.add("C");
    }

    @Override
    public int getRowCount() {
        return list.size();
    }

    @Override
    public int getColumnCount() {
        return 1;
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        return rowIndex + "-" + columnIndex;
    }

    @Override
    public String getColumnName(int column) {
        return "Column " + column;
    }

    @Override
    public Class<?> getColumnClass(int columnIndex) {
        return String.class;
    }

    @Override
    public boolean isCellEditable(int rowIndex, int columnIndex) {
        return false;
    }

    @Override
    public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
        // 待实现
    }
}
异常栈信息
Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException: Cannot invoke "javax.swing.CellRendererPane.paintComponent(java.awt.Graphics, java.awt.Component, java.awt.Container, int, int, int, int, boolean)" because "this.rendererPane" is null
at java.desktop/javax.swing.plaf.basic.BasicTableUI.paintCell(BasicTableUI.java:2191)
at java.desktop/javax.swing.plaf.basic.BasicTableUI.paintCells(BasicTableUI.java:2092)
at java.desktop/javax.swing.plaf.basic.BasicTableUI.paint(BasicTableUI.java:1888)
at com.formdev.flatlaf.ui.FlatTableUI.paint(FlatTableUI.java:397)
at java.desktop/javax.swing.plaf.ComponentUI.update(ComponentUI.java:161)
at java.desktop/javax.swing.JComponent.paintComponent(JComponent.java:797)
at java.desktop/javax.swing.JComponent.paint(JComponent.java:1074)
at java.desktop/javax.swing.JComponent.paintToOffscreen(JComponent.java:5255)
at java.desktop/javax.swing.RepaintManager$PaintManager.paintDoubleBufferedImpl(RepaintManager.java:1643)
at java.desktop/javax.swing.RepaintManager$PaintManager.paintDoubleBuffered(RepaintManager.java:1618)
at java.desktop/javax.swing.RepaintManager$PaintManager.paint(RepaintManager.java:1556)
at java.desktop/javax.swing.RepaintManager.paint(RepaintManager.java:1323)
at java.desktop/javax.swing.JComponent._paintImmediately(JComponent.java:5203)
at java.desktop/javax.swing.JComponent.paintImmediately(JComponent.java:5013)
at java.desktop/javax.swing.RepaintManager$4.run(RepaintManager.java:865)
at java.desktop/javax.swing.RepaintManager$4.run(RepaintManager.java:848)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:391)
at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
at java.desktop/javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:848)
at java.desktop/javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:823)
at java.desktop/javax.swing.RepaintManager.prePaintDirtyRegions(RepaintManager.java:772)
at java.desktop/javax.swing.RepaintManager$ProcessingRunnable.run(RepaintManager.java:1884)
at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:316)
at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:770)
at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:721)
at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:715)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:391)
at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:740)
at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)
Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException: Cannot invoke "javax.swing.JTable.getColumnModel()"
because "this.table" is null
at java.desktop/javax.swing.plaf.basic.BasicTableUI.getPreferredSize(BasicTableUI.java:1768)
at java.desktop/javax.swing.JComponent.getPreferredSize(JComponent.java:1680)
at java.desktop/javax.swing.JTable.setWidthsFromPreferredWidths(JTable.java:3205)
at java.desktop/javax.swing.JTable.doLayout(JTable.java:3117)
at java.desktop/java.awt.Container.validateTree(Container.java:1722)
at java.desktop/java.awt.Container.validateTree(Container.java:1731)
at java.desktop/java.awt.Container.validateTree(Container.java:1731)
at java.desktop/java.awt.Container.validateTree(Container.java:1731)
at java.desktop/java.awt.Container.validate(Container.java:1657)
at java.desktop/javax.swing.RepaintManager$3.run(RepaintManager.java:745)
at java.desktop/javax.swing.RepaintManager$3.run(RepaintManager.java:743)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:391)
at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
at java.desktop/javax.swing.RepaintManager.validateInvalidComponents(RepaintManager.java:742)
at java.desktop/javax.swing.RepaintManager$ProcessingRunnable.run(RepaintManager.java:1883)
at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:316)
at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:770)
at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:721)
at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:715)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:391)
at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:740)
at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)
解决方案

可根据业务场景选择以下两种修复方案:

  • 方案1:严格遵守Swing单线程规则,轻量序列化逻辑直接放到EDT执行。如果序列化逻辑耗时较长怕阻塞UI,可先在子线程把Model中的纯业务数据拷贝到独立的、不关联任何Swing组件的POJO对象中,再在子线程序列化该POJO,避免遍历到Swing UI对象链。
  • 方案2:序列化前先解除Model和可见JTable的绑定。AbstractTableModel本身支持序列化,但当它被设置到JTable上之后,JTable内部的监听器链会反向持有UI相关对象引用,序列化时就会遍历到非线程安全的UI类。可先在EDT中调用table.setModel(临时空Model)把目标Model从可见JTable上解绑,之后即可在任意线程序列化该Model,序列化完成后再在EDT中把Model重新设置回JTable即可。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 17:24:19