Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Concurrency problems in JTable

I have a problem where I have a JTable and a custom model, with concurrent access problems when the model is modified during the rendering phase. I receive an exception like the following, because I assume that it gets the length of the table, the model is updated, and then it accesses a model element that doesn't exist. The AbstractTableModel needs to reaccess the model using a row / column index during rendering to get the required information, and there doesn't seem to be any locking around this, meaning the data can change freely.

Exception in thread "AWT-EventQueue-0" java.lang.IndexOutOfBoundsException: Index: 2, Size: 2
    at java.util.LinkedList.checkElementIndex(LinkedList.java:553)
    at java.util.LinkedList.get(LinkedList.java:474)
    at koku.ui.PlayerList$PlayerInfoTblModel.getValueAt(PlayerList.java:250)
    at javax.swing.JTable.getValueAt(JTable.java:2720)
    at javax.swing.JTable.prepareRenderer(JTable.java:5718)
    at javax.swing.plaf.basic.BasicTableUI.paintCell(BasicTableUI.java:2117)
    at javax.swing.plaf.basic.BasicTableUI.paintCells(BasicTableUI.java:2019)
    at javax.swing.plaf.basic.BasicTableUI.paint(BasicTableUI.java:1815)
    at javax.swing.plaf.ComponentUI.update(ComponentUI.java:161)
    at javax.swing.JComponent.paintComponent(JComponent.java:778)
    at javax.swing.JComponent.paint(JComponent.java:1054)
    at javax.swing.JComponent.paintChildren(JComponent.java:887)
    at javax.swing.JComponent.paint(JComponent.java:1063)
    at javax.swing.JViewport.paint(JViewport.java:725)
    at javax.swing.JComponent.paintChildren(JComponent.java:887)
    at javax.swing.JComponent.paint(JComponent.java:1063)
    at javax.swing.JComponent.paintChildren(JComponent.java:887)
    at javax.swing.JComponent.paint(JComponent.java:1063)
    at javax.swing.JComponent.paintToOffscreen(JComponent.java:5206)
    at javax.swing.BufferStrategyPaintManager.paint(BufferStrategyPaintManager.java:295)
    at javax.swing.RepaintManager.paint(RepaintManager.java:1217)
    at javax.swing.JComponent._paintImmediately(JComponent.java:5154)
    at javax.swing.JComponent.paintImmediately(JComponent.java:4964)
    at javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:781)
    at javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:739)
    at javax.swing.RepaintManager.prePaintDirtyRegions(RepaintManager.java:688)
    at javax.swing.RepaintManager.access$700(RepaintManager.java:59)
    at javax.swing.RepaintManager$ProcessingRunnable.run(RepaintManager.java:1632)
    at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:251)
    at java.awt.EventQueue.dispatchEvent(EventQueue.java:660)
    at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:211)
    at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:128)
    at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:117)
    at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:113)
    at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:105)
    at java.awt.EventDispatchThread.run(EventDispatchThread.java:90)

Wondering what the best way to solve this problem is.

Cheers,
Chris

like image 617
Chris Dennett Avatar asked May 12 '11 18:05

Chris Dennett


2 Answers

This is my approach:

  • There is a Data Model that is updated from background threads. You do not use this model directly from Swing.
  • The Data Model notifies its listeners via events. These events contain everything that is needed - no listeners should make a call to the Data Model to retrieve some value. (side note: for non-GUI purposes, you eventually may want to make direct calls on the Data Model, but for Swing you definately don't want to do that. Either way, it isn't necessary - the events contain everything) One of these listeners will in turn update the Table Model, but on the Event Dispatching Thread, using only information from the event.
  • Then there is the Table Model, which provides various getters for the JTable (getValueAt, getColumnCount, ...). The Table Model infact holds a locally cached copy of the Data Model, and this copy is updated only via incoming events, which are processed on the EDT - because the listener is running on the EDT. It is thus very important not to make direct calls to the underlying Data Model, as this is being updated from other threads - by the time the JTable wants a value for some cell at row X, this row may no longer exist. The only way to get to the actual data is polling the local cache. Thus, the local copy of the Data Model is also being manipulated on the EDT. This is important, because after the local copy was manipulated, you typically invoke some fireTableXxx() method in order to let all views update themselves. As the views will update themselves on the EDT as well, the Table Model cannot be manipulated in this time window: any invokeLater(...) will effectively be executed after the table refreshing has finished.
  • The View, JTable, calls the getters on the TableModel on the EDT.
  • Upon registering a listener, it will receive all necessary events to become synchronized with the Data Model.

In summary, the process of updating the Table Model and refreshing the JTable (and other views, if any) is made an atomic operation. To achieve this, we have a separate cache model backing our tabel, which is updated only on the EDT. Everything Swing-related becomes single threaded, and by using invokeLater() we are sure that the next event is processed only after the current event has been fully processed.

How to improve this even further:

  • Separate the actual EDT model from the JTable, and implement a TableModel by delegating the calls to the EDT model. This way, you can use unlimited Swing listeners on a single EDT model. This is great, as the implementation of Swing models (TableModel, ListModel, ComboBoxModel, etc.) are very small straightforward and understandable implementations, and the DRY principle is satisfied - Don't Repeat Yourself. The EDT model code is centralized, and becomes reusable. Swing models become adapters, and do not store state.
  • Each Swing model is somehow registered on the EDT model.
  • The EDT model notifies each registered Swing model. For example, an AbstractTableModel implementation would react to such notification with notifying the JTable that is listening, by calling fireTableXxxChanged().

In the end you would have this chain:

  • A view on top of the Swing model (e.g. a JTable)
  • A Swing model on top of the EDT model (e.g. an AbstractTableModel)
  • A listener, listening on the EDT model, reacting to EDT model events by sending out higher-level events (e.g. tableModel.fireTableXxxChanged())
  • The EDT model on top of the concurrent model. This model is infact a helper model, and is not the "business logic state" reference. It is infact a snapshot of the actual model underneath, providing a consistent, unchanging state during updates of Swing components. The EDT model is thus a helper model in the GUI layer.
  • A listener, listening to the concurrent model, updating the EDT model on the Event Dispatching Thread. This listener may bundle several concurrently arriving events in one for increased efficiency.
  • The concurrent model that simply does not care about anything Swing / EDT related at all. This model is pure business logic.

This approach allows you to completely separate GUI from business logic (recognize the 3 layer system here: GUI, Business Logic and Persistence), which is very powerful. In a well-designed system where all commands are executed in the 2nd layer, you can create multiple controllers very easily. For example, in a GUI, it becomes possible to manipulate the application state using Swing controls, but also to create a command line where you can just type the commands. This would come in very handy for scripted/automated testing of the business logic, and how the GUI reacts on changes in the business logic.

In the end, it pays off, but it definately requires a lot of extra work and hard thinking to do it right.

like image 81
Timmos Avatar answered Sep 28 '22 22:09

Timmos


Swing component/models should always be updated from the AWT thread, and never from another thread.

See SwingUtilities.invokeLater, and SwingWorker for long running tasks

like image 40
Kaj Avatar answered Sep 28 '22 22:09

Kaj