So, first of all, I absolutely know the main thread safety rule of Swing: all GUI component interaction must take place on the Event Dispatch Thread. But as I've been writing code, and working on following Model-View-Controller guidelines, I've been wanting to find a way to get around that with Swing component data models.
Warning, this is a bit of a long post, but I wanted to really explain my thought process. I’m being very creative here, and mostly just want feedback on if this would actually work or not.
The program I'm working on right now uses a controller class that implements ActionListener to listen to events from components in my UI classes. When the actionPerformed() method is called, it passes the ActionEvent to another thread via an executor, where the event is parsed and the models for my program are updated. The models fire PropertyChangeEvents, which are wrapped in SwingUtilities.invokeLater so the changes they make to the GUI take place on the EDT.
Up until this point, everything seems to be 100% kosher.
But then I have this JList. What I want to do is modify its components off of the EDT, and then update it. I've played with several ideas, and I'm actually exploring multiple avenues of accomplishing this. Special add/remove PropertyChangeEvents or passing new lists to replace the existing one with each event are just some of the ideas I have.
But I'm making this post to bring up the craziest idea I have: a thread-safe, ConcurrentListModel, which can be modified by any number of threads yet still conform to Swing's thread safety rules.
So, let me explain:
For a ListModel, it interacts with the JList itself in only three ways. Each of these ways presents a unique problem for concurrent access:
1) Retrieving information from the underlying collection (getElement, getSize). The JList needs to be able to access the underlying collection to properly display its values in its view. Any synchronization on this underlying collection could cause responsiveness issues with the UI, but if this collection is modified by multiple threads the result is any number of thread consistency issues.
2) Adding/Removing ListDataListeners. The listener being added, of course, is a BasicListUI.ListDataHandler, which is linked to the JList itself. The AbstractListModel implementation allows any number of ListDataListeners to be added to an EventListenerList. Again, this list can't be synchronized when the EDT needs to access it, because of responsiveness problems, but if its modified by multiple threads the consistency of this list's state cannot be guaranteed.
3) Firing ListDataEvents. The JList has no link to the underlying collection in the model, only the model itself. So whenever the model makes a change to the underlying collection, it must fire a ListDataEvent to the JList. The JList then invokes the getElement/size methods I mentioned earlier. These events MUST be fired on the EDT.
So, at first blush, this is a component whose relationship with the JList makes it impossible to modify from off of the EDT. Except... I'm a stubborn SOB, and I think I've figured out how to do this. Everything I'm about to describe going forward, please respond with any obvious errors in judgement I'm making.
The three things I listed, they don't require the ListModel to only be touched by the EDT, they only require that certain parts of it only be touched by the EDT. Specifically, it requires the STATE FIELDS of this class (the underlying list and the EventListenerList) to be kept thread-local on the EDT. All the methods of the class interact with those two fields in one way or another; so long as the effects those methods ultimately have on those fields are performed only on the EventDispatchThread, it doesn’t matter what thread they are invoked it.
Moreover, since the class’s state is only being modified on a single thread, there is no need for any synchronization at all, since the state fields are being kept thread-local. This avoids the potential responsiveness errors that could result from Swing components waiting on a lock.
So, what do I ultimately mean by all this? I’m going to show off my sample code so you can really see what I’m getting at. Here is a simple add() method following my design (NOTE: I also want the final product to follow the Collections API and List interface, hence the method signatures):
public boolean add(final E element){
final boolean result = false;
if(SwingUtilities.isEventDispatchThread()){
result = swingAdd(element);
}
else{
SwingUtilities.invokeLater(new Runnable(){
@Override
public void run(){
result = swingAdd(element);
}
});
}
return result;
}
private boolean swingAdd(E element){
int index = list.size(); //The current size will be size - 1 after the add, aka the index of the element being added.
boolean result = list.add(element);
fireContentsChanged(this, index, index);
return result;
}
From this example, you can see how I'm accomplishing this. Each public method is paired with a separate, private "swing" method. The public method performs a check to see if its being invoked on the EDT. If it is, it simply runs the swing method. If it isn't, it uses SwingUtilities.invokeLater() and then calls the swing method. Either way, the swing method is only called on the EDT. And since the swing method is the only part of this that actually interacts with the underlying list, the list is kept thread-local.
The only major requirement for a class that utilizes this design would be that it is ONLY instantiated on the EventDispatchThread. It can be passed to or utilized on other threads after that, but the last guarantee of the thread-safety of its underlying state needs to come from instantiation on the EDT. By following that one rule, this class will guarantee thread-safe, swing-safe concurrent access to a list model that is already attached to a JList in the following ways:
1) The state of the underlying list is thread-local. The list wrapped in this model will only ever be interacted with on the EventDispatchThread, thus preserving the thread-confinement of swing.
2) No synchronization is needed. Synchronization is intended to protect mutable state, but the mutable state of this class will be protected by only being accessed on the EventDispatchThread. This both facilitates concurrent access and avoids UI lag by preventing the need for lock acquisition.
3) The EventListenerList will be equally protected on the same level as the underlying list. All add/remove operations will ultimately be performed only on the EDT.
4) ListDataEvents will only ever be fired on the EDT, ensuring that the swing components that its attached to will only ever be interacted with on the EDT.
5) If this class is accessed on the EDT, SwingUtilities.invokeLater() will not be used. This avoids unnecessary queued runnables when an operation that doesn't require it can be performed.
Right now, there are only three things that I'm still working on resolving before I'm gonna write the class itself. Two are relatively minor details, but one could be an issue:
1) (Minor) What if a ListDataEvent is fired to a non-swing component. I don't want the EDT to get bogged down with what should be potential background processes. If I can't figure out a thread-safe solution for this, I'll just cut off the ability of a ListDataListener to be added from a non-EDT thread and take other steps to restrict the type of listener object that can be used with this, which is why I regard this as a minor issue.
2) (Minor) I want to also fully implement the List interface with this class. I haven't really figured out the iterator and list iterator part of it, but then again, I haven't spent much time thinking about that part yet. Ultimately, I could just leave that out and have those methods return null or throw an unsupportedexception.
3) (Potential Issue) The references to the objects contained in the underlying list won't have any known thread safety guarantees. That is, whether they are thread-safe or not, the state of an individual element could be modified from multiple threads even while its position in the list remains unchanged. Moreover, such objects could use synchronization to access their internal state, but that sort of thing sounds like something that is pretty standard for ListModels to have to deal with in swing.
Anyway, putting this aside to deal with some other stuff for now, but looking for feedback. Am I onto something, or totally out of my goddamn mind? lol.