开发者

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:6开发者_开发知识库88)
    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


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.


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


I'd suggested using Glazed Lists for all TableModel access: http://www.glazedlists.com/

I've used them on a number of projects for some pretty heavy lifting of data and it has worked flawlessly. It abstracts TableModels to an ArrayList which you can wrap in SynchronizedTableLists and FilteredLists that let you do all sorts of really complicated things very easily and safely.

You can also add Listeners and get notified of modifications to the TableModel


if you are going to do concurrent access you need to synchronize your model. try reading the tutorial http://download.oracle.com/javase/tutorial/essential/concurrency/ good luck

PS: Sometimes you can think of another solution for your software than going for concurrent access. Also, to get better answers you could post some code of your application.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜