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
相关产品推荐
相关产品推荐

