How to synchronize Swing model with a rapidly changing "real" model?
As is widely known, anything related to Swing components must be done on the event dispatch thread. This also applies to the models behind the components, such as TableModel. Easy enough in elementary cases, but things become pretty complicated if the model is a "live view" of something that must run on a separate thread because it's changing quickly. For example, a live view of a stock market on a JTable. Stock markets don't usually happen on the EDT.
So, what is the preferable pattern to (de)couple t开发者_如何转开发he Swing model that must be on the EDT, and a "real", thread-safe model that must be updateable from anywhere, anytime? One possible solution would be to actually split the model into two separate copies: the "real" model plus its Swing counterpart, which is is a snapshot of the "real" model. They're then (bidirectionally) synchronized on the EDT every now and then. But this feels like bloat. Is this really the only viable approach, or are there any other, or more standard, ways? Helpful libraries? Anything?
I can recommend the following approach:
- Place events that should modify the table on a "pending event" queue, and when an event is placed on the queue and the queue is empty then invoke the Event Dispatch thread to drain the queue of all events and update the table model. This optimisation means you are no longer invoking the event dispatch thread for every event received, which solves the problem of the event dispatch thread not keeping up with the underlying event stream.
- Avoid creation of a new Runnable when invoking the event dispatch thread by using a stateless inner class to drain the pending event queue within your table panel implementation.
- Optional further optimisation: When draining the pending event queue minimise the number of table update events fired by remembering which table rows need to be repainted and then firing a single event (or one event per row) after processing all events.
Example Code
public class MyStockPanel extends JPanel {
private final BlockingQueue<StockEvent> stockEvents;
// Runnable invoked on event dispatch thread and responsible for applying any
// pending events to the table model.
private final Runnable processEventsRunnable = new Runnable() {
public void run() {
StockEvent evt;
while ((evt = stockEvents.poll() != null) {
// Update table model and fire table event.
// Could optimise here by firing a single table changed event
// when the queue is empty if processing a large #events.
}
}
}
// Called by thread other than event dispatch thread. Adds event to
// "pending" queue ready to be processed.
public void addStockEvent(StockEvent evt) {
stockEvents.add(evt);
// Optimisation 1: Only invoke EDT if the queue was previously empty before
// adding this event. If the size is 0 at this point then the EDT must have
// already been active and removed the event from the queue, and if the size
// is > 0 we know that the EDT must have already been invoked in a previous
// method call but not yet drained the queue (i.e. so no need to invoke it
// again).
if (stockEvents.size() == 1) {
// Optimisation 2: Do not create a new Runnable each time but use a stateless
// inner class to drain the queue and update the table model.
SwingUtilities.invokeLater(processEventsRunnable);
}
}
}
As far as I understand, you don't want to implement Swing model interfaces in your real model, do you? Can you implement a Swing model as a "view" over a part of a real model? It will translate its read-access getValueAt() to the calls of the real model, and the real model will notify Swing model about the changes , either providing a list of changes or assuming that Swing model will take care of quering the new values of everything it currently is showing.
The usual approach is to send "signals" of some kind to which the UI listens. In my code, I often use a central dispatcher which sends signals that contain the object that was modified, the name of the field/property plus the old and new value. No signal is sent for the case oldValue.equals(newValue)
or oldValue.compareTo(newValue) == 0
(the latter for dates and BigDecimal
).
The UI thread then registers a listener for all signals. It then examines the object and the name and then translates that to a change in the UI which is executed via asyncExec()
.
You could turn that into a listener per object and have each UI element register itself to the model. But I've found that this just spreads the code all over the place. When I have a huge set of objects on both sides, I sometimes just use several signals (or events) to make things more manageable.
精彩评论