Play / Watch, Sync, and Parallel Collections

When implementing thread-safe listeners, I usually wonder what type of Collection best to hold listeners. So far I have found three options.

The Observable standard 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 as it prevents issues like

  • The Listener removes itself in the callback ( ConcurrentModificationException - can iterate through the indexed for loop in the reverse order to prevent this)
  • Executing external code inside a synchronized block can block everything.

The implementation, unfortunately, is more than a few lines. Collections.synchronizedList() will not require synchronized in removeListener , but it really is not 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 is a Collection in java.util.concurrent that is thread safe and potentially more efficient in its essence, as I would suggest that their internal locking mechanism is better optimized than a simple synchronized block. Making a copy for each 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(); } } } 

You should do something like the first version does, but with fewer copies. Adding / removing listeners is not a very frequent action, which means that copies should not be very frequent either.

I usually use a version based on ConcurrentHashMap that points to .keySet() , which is used here as Set here:

A view iterator is a “weakly matched” iterator that will never throw a ConcurrentModificationException and ensures that the elements intersect as they existed during the construction of the iterator, and can (but not guaranteed) reflect any changes after construction.

This means that the iteration includes at least every listener that was registered at the start of the iteration, and may even include new ones that were added during the iteration. Not sure if listeners have been deleted. I like this version because it is not copied and not just implemented 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 am I missing something? Or is there an even better Collection to use?

+4
source share
2 answers

Is a ConcurrentHashMap based implementation a good idea or am I missing something? Or is there an even better collection to use?

ConcurrentHashMap seems to be good in this case. This is certainly better than using Collections.synchronizedList() . The CHM iterator may or may not see new entries on the map if they are added when the iterator goes on the map. It can also see or not see old records if they are deleted during iteration. Both of them are caused by race conditions when the added / removed nodes were located and where the iterator is located. But the iterator will always see the elements that were on the map when they started until they were deleted.

+2
source

There is a template that surpasses all of these options since Java 1.1. Take a look at the AWTEventMulticaster class and how it works. It ensures thread safety through immutability and therefore does not create additional overhead. It even provides the most efficient way to handle a case when there is not or exactly one listener. Well, I think it offers the most efficient event delivery overall.

See http://docs.oracle.com/javase/7/docs/api/java/awt/AWTEventMulticaster.html

Check out the source code for this class if you want to know how to implement such a template for your own event types.

It is a pity that the Swing developer did not know about this and created this terrible EventListenerList of the leading developers in the wrong way.

And by the way, this template also solves the problem that listeners added during event delivery should not see current events (events) that occurred before the addition. Is free. Since Java 1.1

+2
source

All Articles