Sorting out listener implementations in Java
September 21, 2012 | 3 min ReadRecently I scanned the source code of our current Eclipse RCP/RAP project for occurrences of the Listener (or Observer) pattern. I found no less than 6 (!) different implementations and decided to unify them into one. When researching the implementation options (and the Java world has many to offer), I found an aspect of Listener programming that is usually not mentioned by internet resources. But first, let’s look at the options.
In general, the solutions for thread safety can be divided into three classes: heavyweight, lightweight or no synchronization.
No synchronization
private final List listeners = new ArrayList();
public void addListener(Listener listener) {
listeners.add(listener);
}
public void removeListener(Listener listener) {
listeners.remove(listener);
}
private void notifyListeners() {
for (Listener listener : listeners) {
listener.doSomething();
}
}
This solution works fine in a single-threaded environment but doesn’t work well in a multi-threaded application. When one thread is adding or removing a listener while another thread is looping over the listeners
, you will get the infamous java.lang.ConcurrentModificationException. So, some synchronization is needed.
Heavyweight synchronization
Here, the number of threads accessing the list is limited by locks. The most common example is the use of Java’s synchronized
keyword.
private final List listeners = new ArrayList();
public void addListener(Listener listener) {
synchronized (listeners) {
listeners.add(listener);
}
}
public void removeListener(Listener listener) {
synchronized (listeners) {
listeners.remove(listener);
}
}
private void notifyListeners() {
synchronized (listeners) {
for (Listener listener : listeners) {
listener.doSomething();
}
}
}
This approach works well but is not generally regarded as the best solution. While one thread has the lock, the others have to wait, which can slow down your application considerably. Additionally there is always risk of deadlocks when synchronized
comes into play.
Lightweight synchronization With Lightweight synchronization, the idea is to loop over a copy of the list or to use a list which allows concurrent modification. This makes explicit locking unnecessary. There are many implementations for this pattern in the Java world. Just to name a few:
- Replace the
ArrayList
with java.util.concurrent.CopyOnWriteArrayList - Replace the
ArrayList
with java.util.concurrent.ConcurrentLinkedQueue - In Swing use javax.swing.event.EventListenerList
- In Eclipse RCP/RAP use org.eclipse.core.runtime.ListenerList
Each of these approaches have their pros and cons which are usually described well in their JavaDoc or the numerous articles and blog posts about the topic. What’s not usually mentioned is one characteristic all of these solutions have in common: if a listener is removed while another thread notifies the listeners, there is no guarantee that the removal of the listener is reflected in the ongoing notification.
This means in practice that methods on a listener could be called after it has been removed! Imagine the case that one thread removes the listener from the list and releases all resources the listener is holding (which seems ok since they are not needed anymore). Then another thread finds the listener in the (outdated) listener list and executes a method which requires the resources which have already been released.
Now, what is our conclusion here? Either you use heavyweight synchronization or, when using lightweight synchronization, you have to make sure that:
- the
notifyListeners
should be programmed in a way that it can handle exceptions coming from the listeners, i.e. a try-catch around the listener method call - all public methods in the listener should gracefully handle the case that they are called although the listener is already marked for disposal
The first point is a best practice for listener implementations anyway. The second is probably not an problem for most listeners but it’s important to keep it in mind when programming - bugs related to multi-threading are usually Heisenbugs and hard to fix.