Skilled Listening in Java


Dr. Warren D. MacEvoy
Mesa State College
Grand Junction, CO USA

Outline

  1. Upshot
  2. Intro
  3. Correct
  4. And Efficient
  5. What’s been said...

Upshot

There are many good resources on using events and implementing event listeners. Unfortunately, there are nearly as many resources that incorrectly or inefficiently implement event generators. These implementations introduce subtle nondeterministic errors, execute slowly, and/or fill the virtual machine with unnecessary garbage. The goal of this article is to address this problem once and for all; as a pattern and code generator for JDK’s before 1.5, and as a set of generic types in JDK 1.5 and later.

Intro

Most Java programmers learn how to Listen early on, usually thanks to the Swing classes. The elements are as follows:

Abstractly, at any given time, the set of generators and listeners for an event form a directed graph, where edges go from generators to listeners. Usually, this graph is essentially static. That is, listeners are added to generators as part of some initialization phase, and events don't start moving through the graph until after all the connections have been made. Almost all of the implementations you see of generators work correctly, although not necessarily efficiently, in this situation.

But here's the rub. These things are often not correctly accounted for:

  1. Adding and removing listeners may happen asynchronously. That is, different threads may add or remove listeners at somewhat inconvenient times.
  2. In response to listening to an event (and so synchronously), a listener may add or remove itself or other listeners from a generator.
  3. Generators may choose to send events asynchronously.
These are all issues of correctness. Issues of efficiency are
  1. Most generators spend most of their life sending events (as opposed to adjusting who is listening).
  2. The average number of listeners for each generator is significantly less than 1.

The humble truth is, most of the generators in your application are not being listened to by anything, and most of the rest have one listener. For example, of the 18 events generated by a JButton, how many do you actually pay attention to?

Correct

Correct is more important than efficient. Below is a typical implementation of a correct, but inefficient, generator:

interface Event { /* whatever you need */ }

interface Listener { void receive(Generator from, Event event); }

interface Generator { 
    void addListener(Listener listener);
    void removeListener(Listener listener);
}

class DefaultGenerator implements Generator {
    private ArrayList listeners = new ArrayList();
    private Object lock = new Object();
    public void addListener(Listener listener) {
        synchronized(lock) {
            if (listener != null &&
                ! listeners.contains(listener)) {
                listeners.add(listener); 
            }
        }
    }

    public void removeListener(Listener listener) {
        synchronized(lock) {
            listeners.remove(listener);
        }
    }

    protected ArrayList cloneListeners() {
        synchronized(lock) {
            return (ArrayList) listeners.clone();
        }
    }

    protected void send(Event event) {
        ArrayList tmp = cloneListeners();
        for (Iterator i = tmp.iterator(); i.hasNext(); ) {
            ((Listener) i.next()).receive(this,event);
        }
    }
} // class DefaultGenerator

Lets see why this is correct. First, the synchronized(lock) blocks synchronize access to the listeners list. It is very important that only the clone step of send is synchronized. This is because completing a send step may require arbitrary adds or removes. If these adds or removes happen from a different thread compared to the send, they would deadlock if send(Event) is synchronized. To avoid this problem, we copy the listeners list before notifying them, thus eliminating the resource contention. It is also important that we do not use the synchronized keyword in front of the add, remove, and clone methods. This is because using synchronized in front of a method uses the object itself for mutually exclusive access to the methods. Since you have no control over how the DefaultGenerator will be used, it is pretty easy to set up a deadlock situation by having this public mutex locked by something else. By sticking to an internal private object for a lock, we know we’re the only ones who affect it.

So safety pays in correctness (which is a good thing), but it suffers from some serious inefficiencies:

  1. The listeners list is cloned on every send. Most of the time, nobody changes the listeners during a send, so the clone does not usually matter.
  2. An Iterator is created on every send. This is another object to create and discard. Using a (pre JDK 1.5) iterator also requires run-time safety checks while going through the List.
  3. An Event is passed to every send. Most of the time, nobody is listening to the generator, so that event usually just lands in the garbage pile.

And Efficient

Sometimes correctness costs efficiency, but we are lucky here, because we can have both:

class DefaultGenerator implements Generator {
    private static final Listener[] NONE = new Listener[0];
    private Listener [] listeners = NONE;
    private Object lock = new Object();

    private boolean contains(Listener listener) {
        Listener[] tmp = listeners;
        for (int i=tmp.length-1; i>=0; --i) 
            if (tmp[i] == listener) return true;
        return false;
    }

    // if we don't already have it,
    // add a listener to the front of a new listeners array.
    public void addListener(Listener listener) { 
        synchronized(lock) {
            if (listener == null || contains(listener)) return;

            Listener[] tmp = new Listener[listeners.length+1];
            for (int i=tmp.length-1; i>0; --i) tmp[i]=listeners[i-1];
            tmp[0]=listener;
            
            listeners=tmp; // atomic update
        }
    }

    // if we do already have it,
    // remove a listener from a new listeners array.
    public void removeListener(Listener listener) { 
        synchronized(lock) {
            if (!contains(listener)) return;

            Listener[] tmp = (listeners.length > 1) ?
                new Listener[listeners.length-1] : NONE;

            for (int i=listeners.length-1,j=i; i >= 0; --i) 
                if (listeners[i] != listener) 
                    tmp[--j]=listeners[i];

            listeners=tmp;  // atomic update
        }
    }

    protected final boolean listening() { 
        return listeners != NONE; 
    }

    protected final void send(Event event) { 
        Listener[] tmp=listeners; // atomic read
        for (int i=tmp.length-1; i>=0; --i) {
            tmp[i].receive(this,event);
        }
    }

} // class DefaultGenerator

To use this DefaultGenerator, we would write:

class MyGenerator extends DefaultGenerator {
    void send() { if (listening()) send(new Event()); }
}

There is more code, but what have we gained? This implementation is also correct, but look at send(), listening() and send(Event): no synchronization, no copies, no type-casts, and we only create an Event if (almost always - see callout) we have listeners to receive it. Instead of duplicating the listener list every time a send happens, we instead make copies when adding and removing from the listeners array. This works because object reference assignment is an atomic action. We use a plain old array because this eliminates unnecessary run-time type casts and the iterator overhead.

“almost always”

I stated that, almost always,

if (listening()) send(new Event())
will create and send an event only when there are listeners to receive it. But notice that there is no synchronization between the listening() and the send(new Event()), and so the listeners list may change arbitrarily between the check for listeners and the send. First, realize this is a very subtle timing issue and so, almost always, the listening() call will correctly determine if there is anyone listening. Second, realize this does not affect the correctness of the generators, because only two things will (very rarely) happen:
So we have a generator that is still safe, but faster than the naïve safe solution. But it's exactly the kind of code that is easy to get wrong. And why would we want to rewrite something a bunch of times anyway? JDK 1.5 helps us with the use of generics. Instead of using the above pattern again and again for each kind of event, we can write it once and be done. An implementation is given in the resources.

With the generics at hand, you can stop worrying about making correct and fast generators and just use them. For example, if you wanted to a DateGenerator that sends the current Date on every tick(), a listener that printed out the date, and some main code to put them together:

class DateGenerator extends DefaultGenerator < Date > {
    public void tick() { if (listening()) send(new Date()); }
}

class DateListener implements Listener < Date > {
    public void receive(Generator < Date > generator , Date date) {
        System.out.println(date);
    }
}

public class Main {
    public static void main(String[] args)
    {
        DateGenerator generator = new DateGenerator();
        DateListener listener = new DateListener();

        generator.addListener(listener);
        generator.tick();
    }
}

With the generic implementation, you can think about the actual messages and what they do, rather than the details of the mechanism for every new message type.

For those who are required to be compatible with a pre 1.5 JDK, it can be a lot of error prone typing to write the boiler-plate code for every Event type you happen to need. In the resources, there is a tool for writing this code (built using the BeanShell Preprocessor, but you don’t need BPP or BeanShell to use it).

What’s been said...

So. Generators are poorly or incorrectly implemented all the time. Using these generators result in solutions that are less stable, slower, and consume more memory. For older (pre 1.5) JDK’s, this can be solved by adopting the fast generator pattern. These patterns can be built automatically using meta.GenerateListeners. For new (post 1.5) JDK’s, this can be solved using generic classes. These tools are given in the resources.

That’s it: happy listening in a dynamic world!

Resources

Online version of this paper

JavaDOC's for meta package

JavaDOC's for classes generated by meta package

JavaDOC's for events package

Source/binary/doc zip file

Writing Event Listeners