Reputation: 63955
When implementing thread-safe listeners I usually wonder which type of Collection
is the best to hold the listeners. I've found three options so far.
The standard Observable
uses synchronized
access to a simple ArrayList
. Using a copy of the listeners is optional but as far as I can tell a good idea since it prevents problems like
ConcurrentModificationException
- could iterate via indexed for
loop in inverse order to prevent that)Implementation is unfortunately more than a few lines. Collections.synchronizedList()
would not require synchronized
in removeListener
but that's not really worth it.
class ObservableList {
private final List<Listener> listeners = new ArrayList<Listener>();
public void addListener(Listener listener) {
synchronized (listeners) {
if (!listeners.contains(listener)) {
listeners.add(listener);
}
}
}
public void removeListener(Listener listener) {
synchronized (listeners) {
listeners.remove(listener);
}
}
protected void notifyChange() {
Listener[] copyOfListeners;
synchronized (listeners) {
copyOfListeners = listeners.toArray(new Listener[listeners.size()]);
}
// notify w/o synchronization
for (Listener listener : copyOfListeners) {
listener.onChange();
}
}
}
But there are Collection
s in java.util.concurrent
that are inherently thread-safe and potentially more efficient since I would assume that their internal locking mechanism is optimized better than a simple synchronized
block. Creating a copy for every notification is also quite expensive.
Based on CopyOnWriteArrayList
class ObservableCopyOnWrite {
private final CopyOnWriteArrayList<Listener> listeners = new CopyOnWriteArrayList<>();
public void addListener(Listener listener) {
listeners.addIfAbsent(listener);
}
public void removeListener(Listener listener) {
listeners.remove(listener);
}
protected void notifyChange() {
for (Listener listener : listeners) {
listener.onChange();
}
}
}
Should do roughly what the first version does but with less copies. Adding / removing listeners is not a very frequent action which means that copies should not be very frequent as well.
The version I usually use is based on ConcurrentHashMap
which states for .keySet()
which is used as Set
here:
The view's iterator is a "weakly consistent" iterator that will never throw ConcurrentModificationException, and guarantees to traverse elements as they existed upon construction of the iterator, and may (but is not guaranteed to) reflect any modifications subsequent to construction.
Which means that iteration includes at least every listener that was registered when starting the iteration and may even include new ones that were added during iteration. Not sure about listeners that were removed. I do like this version because it's not copying and as simple to implement as CopyOnWriteArrayList
.
class ObservableConcurrentSet {
private final Set<Listener> listeners = Collections.newSetFromMap(new ConcurrentHashMap<Listener, Boolean>());
public void addListener(Listener listener) {
listeners.add(listener);
}
public void removeListener(Listener listener) {
listeners.remove(listener);
}
protected void notifyChange() {
for (Listener listener : listeners) {
listener.onChange();
}
}
}
Is a ConcurrentHashMap
based implementation a good idea or did I overlook something here? Or is there an even better Collection
to use?
Upvotes: 4
Views: 3227
Reputation: 116858
Is a ConcurrentHashMap based implementation a good idea or did I overlook something here? Or is there an even better Collection to use?
ConcurrentHashMap
seems to be to be fine in this case. It certainly is better than using Collections.synchronizedList()
. The CHM iterator may or may not see new entries in the map if they are added while the iterator is walking the map. It also may or may not see old entries if they are deleted during iteration. Both of these are due to race conditions as to when the nodes in question were added/removed and where the iterator is positioned. But the iterator will always see items that were in the map when it began as long as they were not deleted.
Upvotes: 2
Reputation: 298123
There is a pattern that outperforms all of these variants since Java 1.1. Look at the AWTEventMulticaster
class and how it works. It offers thread-safety through immutability and has therefore no additional overhead. It even provides the most efficient way to handle the case when there is no or exactly one listener. Well, I think it offers the most efficient event delivery in general.
See http://docs.oracle.com/javase/7/docs/api/java/awt/AWTEventMulticaster.html
Look into the source code of this class if you want to know how to implement such pattern for own event types.
It is a pity that the Swing developer did not know about this and created that awful EventListenerList leading developers on a wrong path.
And by the way this pattern also solves the problem that listeners added during event delivery should not see the currently delivered event(s) which occurred before the addition. For free. Since Java 1.1
Upvotes: 2