First lets design a simple event system which consists of a few parts:
- Data - the data which someone cares about
- Event - A signal from a dispatcher to a listener signifying that some data has changed
- Listener - An object which cares about some data and wants to get events about when this data changes
- Dispatcher - The object which has some data that can be listened to. The dispatcher is also responsible for sending events to listeners when data changes
public class Data { private String name; public Data(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
public class DataEvent { private final DataDispatcher source; public DataEvent(DataDispatcher source) { this.source = source; } public DataDispatcher getSource() { return source; } }
public interface DataListener { public void dataChanged(DataEvent event); }
/* * Implementation of our DataListener */ public class SomeoneWhoCares implements DataListener { public SomeoneWhoCares() { } public void dataChanged(DataEvent event) { DataDispatcher dispatcher = event.getSource(); for (Data data : dispatcher.getData()) { System.out.println("Data name: " + data.getName()); } dispatcher.removeDataListener(this); } }
import java.util.ArrayList; import java.util.List; public class DataDispatcher { private final List<DataListener> listeners; private final List<Data> dataList; public DataDispatcher() { listeners = new ArrayList<DataListener>(); dataList = new ArrayList<Data>(); } public void addDataListener(DataListener listener) { listeners.add(listener); } public void removeDataListener(DataListener listener) { listeners.remove(listener); } protected void fireDataEvent(DataEvent event) { for (DataListener listener : listeners) { listener.dataChanged(event); } } public List<Data> getData() { return dataList; } public void addData(Data data) { dataList.add(data); fireDataEvent(new DataEvent(this)); //tell the listeners that the Data has changed } public static void main(String[] args) { DataDispatcher dispatcher = new DataDispatcher(); SomeoneWhoCares someoneWhoCares = new SomeoneWhoCares(); dispatcher.addDataListener(someoneWhoCares); //someoneWhoCares will not get events when DataDispatcher changes //create some data Data john = new Data("John Smith", 12); Data bob = new Data("Bob Thompson", 8); Data adam = new Data("Adam Wells", 3); //add the data to the Dispatcher which causes DataEvents to be fired dispatcher.addData(john); dispatcher.addData(bob); dispatcher.addData(adam); } }This code looks simple and correct on the surface, however there is a fundamental problem which causes the following exception to be thrown during runtime:
Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:782) at java.util.ArrayList$Itr.next(ArrayList.java:754) at DataDispatcher.fireDataEvent(DataDispatcher.java:23) at DataDispatcher.addData(DataDispatcher.java:34) at DataDispatcher.main(DataDispatcher.java:48)
This exception is not very helpful, if you look at where the exception is throw from my code (at DataDispatcher.fireDataEvent(DataDispatcher.java:23)) this is the for loop of the fireDataEvent() method which doesn't make much sense at the first look. Lets focus on the event dispatching and listening code, below is the snippet in focus:
public class SomeoneWhoCares implements DataListener { ... public void dataChanged(DataEvent event) { DataDispatcher dispatcher = event.getSource(); for (Data data : dispatcher.getData()) { System.out.println("Data name: " + data.getName()); } dispatcher.removeDataListener(this); } } public class DataDispatcher { private final List<DataListener> listeners; ... public void removeDataListener(DataListener listener) { listeners.remove(listener); } protected void fireDataEvent(DataEvent event) { for (DataListener listener : listeners) { listener.dataChanged(event); } } ... }
Now SomeoneWhoCares is the only listener registered for DataEvent's on the DataDispatcher, so when fireDataEvent() is called he is the only one gets called, so it must be something he is doing that is causing the exception to happen. If you notice at the end of SomeoneWhoCares.dataChanged() the listener unregister's himself from the DataDispatcher which modifies the listeners list inside of DataDispatcher. But, why does this throw an exception?
To answer this question we need to understand what the for-each loop in java actually means. When a for-each loop is compiled into byte code the compiler translates the for-each loop into a while loop using an iterator from the java.util.Collection interface. So for our example, DataDispatcher.fireDataEvent() would effectively translate into something like:
protected void fireDataEvent(DataEvent event) { Iteratoriter = listeners.iterator(); while (iter.hasNext()) { DataListener listener = iter.next(); listener.dataChanged(event); } }
Now that we understand what the for-each loop really means we can investigate why calling SomeoneWhoCares.dataChanged() causes a java.util.ConcurrentModificationException. This exception is caused by the dispatcher.removeDataListener(this) line from SomeoneWhoCares.dataChanged(), which removes the DataListener from DataDispatcher's listeners list. The exception isn't thrown though when the listener is removed, it is thrown on the next iteration through the for loop when we access our iterator. This is because java's iterator's are fail-fast iterators, which means that from the time the iterator is created, until the time that it is garbage collected, if there is any modification to the collection that they are iterating over an exception will be thrown during the next attempt to use the iterator.
So, now that we know the problem, how do we fix it? Obviously we can't use iterators (ListIterator's suffer from the same problem), so instead we have two choices. The first choice is to use a collection designed for concurrent modification's, java.util.concurrent.CopyOnWriteArrayList. This collection's iterator's are guaranteed to not throw exceptions during iteration if a modification is made to the list, however this comes at a serious performance penalty if the list is modified frequently. Every time a CopyOnWriteArrayList is modified the entire list is copied into a new array (hence the name CopyOnWrite...) which can be seriously detrimental to the performance of an application if frequent modifications occur. However if this list is set in stone and never modified then using a CopyOnWriteArrayList in place of our ArrayList will solve our problem.
public class DataDispatcher { private final List<DataListener> listeners; ... public DataDispatcher() { this.listeners = new CopyOnWriteArrayList<DataListener>(); ... } ... protected void fireDataEvent(DataEvent event) { for (DataListener listener : listeners) { listener.dataChanged(event); } } ... }
However there is a second option for those of us who need to have the ability to modify our collection of listeners frequently. There is a nice method in the list interface which is going to help solve all of our problems, that method is List.get(index i). With this method we can access the elements in our list without the use of an iterator and therefore be able to modify our listeners list during event firing. Lets look at a sample implementation:
protected void fireDataEvent(DataEvent event) { for (int i = 0; i < listeners.size(); i++) { listeners.get(i).dataChanged(event); } }This seems like a very reasonable implementation, however we need to think about the case where a listener removes himself during the firing process (just like SomoneWhoCare's does). Lets say we have a two listeners in our list: List: listenerA[0], listenerB[1] We start firing and our index i starts off at 0. Now listenerA decides to remove himself, the list becomes: List: listenerB[0] Now the end of the loop occurs, i gets incremented to two, the size of the list is 1, so the loop exists without calling listenerB! Here is a simple solution to prevent this problem. Iterate over the list backwards! By iterating backwards, we can guarantee that if listeners remove themselves that all other listeners will be called. Below is a revised sample:
protected void fireDataEvent(DataEvent event) { for (int i = listeners.size() - 1; i >= 0; i--) { listeners.get(i).dataChanged(event); } }So, if you are planning on implementing your own event dispatching system, please be aware that listeners may modify the listeners collection during iteration and your dispatching system needs to be able to handle that! Here is a summary of tips:
- In Java, avoid iterators
- Use CopOnWriteArrayList if you can ensure no (or extremely infrequent) modifications
- Use List.get(index i) method to help solve your lack of iterators
- Iterate backwards so listeners can remove themselvs
public class DataDispatcher { private final List<DataListener> listeners; private final List<Data> dataList; public DataDispatcher() { listeners = new ArrayList<DataListener>(); dataList = new ArrayList<Data>(); } public void addDataListener(DataListener listener) { listeners.add(listener); } public void removeDataListener(DataListener listener) { listeners.remove(listener); } protected void fireDataEvent(DataEvent event) { for (int i = listeners.size() - 1; i >= 0; i--) { listeners.get(i).dataChanged(event); } } public List<Data> getData() { return dataList; } public void addData(Data data) { dataList.add(data); fireDataEvent(new DataEvent(this)); //tell the listeners that the Data has changed } public static void main(String[] args) { DataDispatcher dispatcher = new DataDispatcher(); SomeoneWhoCares someoneWhoCares = new SomeoneWhoCares(); dispatcher.addDataListener(someoneWhoCares); //someoneWhoCares will not get events when DataDispatcher changes //create some data Data john = new Data("John Smith", 12); Data bob = new Data("Bob Thompson", 8); Data adam = new Data("Adam Wells", 3); //add the data to the Dispatcher which causes DataEvents to be fired dispatcher.addData(john); dispatcher.addData(bob); dispatcher.addData(adam); } }
No comments:
Post a Comment