Speed up listener notification

Discover the fastest way to notify event listeners defined by the JavaBeans 1.01 specification

Java 1.1, Swing, JavaBeans, and the InfoBus spec all use the delegation event model described in the JavaBeans 1.01 specification for event registration and notification (see the Resources section below for more information). Under this model, when implementing a JavaBean, you have to implement event registration and event notification code in the bean. In this article, we'll compare the performance of several different techniques for implementing the event registration and notification code. The article is organized into two parts. The first covers the requirements for event delivery and explains the basic technique for implementing fast listener notification code. The second builds a test harness and several test cases that compare the performance of the various implementations. The article concludes by giving advice on when you should apply the optimizations presented.

Requirements for event notification

Let's begin with a quick review of listener notification, which is in fact just another name for event delivery. Event delivery is in turn fully described in Section 6.6 of the JavaBeans 1.01 specification (see Resources).

When a JavaBean wants to trigger an event, it notifies all registered listeners by calling the event's method on the registered listeners' listener interfaces. Listeners register with a JavaBean by calling the bean's add<ListenerType>(<ListenerType> listener)method.

Section 6.5.1 of the JavaBeans 1.01 specification contains an example of event-listener registration and event-listener notification. The source code from this example forms the basis of my analysis:

public interface ModelChangedListener extends java.util.EventListener { void modelChanged(EventObject e); }

public abstract class Model { private Vector listeners = new Vector(); // list of Listeners public synchronized void addModelChangedListener(ModelChangedListener mcl) { listeners.addElement(mcl); } public synchronized void removeModelChangedListener(ModelChangedListener mcl) { listeners.removeElement(mcl); } protected void notifyModelChanged() { Vector l; EventObject e = new EventObject(this); // Must copy the Vector here in order to freeze the state of the // set of EventListeners the event should be delivered to prior // to delivery. This ensures that any changes made to the Vector // from a target listener's method, during the delivery of this // event will not take effect until after the event is delivered synchronized(this) { l = (Vector)listeners.clone(); } for (int i = 0; i < l.size(); i++) { // deliver it! ((ModelChangedListener)l.elementAt(i)).modelChanged(e); } } }

The JavaBeans 1.01 specification specifically states that the list of listeners can potentially be updated at the same time that those very listeners are being notified of events. The target listener may modify the listener list, or a different thread may attempt to add or remove listeners.

Thus, the JavaBeans 1.01 specification allows for different implementations. An implementation can make a copy of the list of listeners, and then notify only the listeners in the copied list of events; alternately, an implementation can use the current (and potentially changing) list as it proceeds with the notifications. Using a copy means that additions and removals that take place during the notification process do not alter the list of listeners being notified of the current event. Notice that the example above uses just such a copy implementation.

In contrast to the JavaBeans 1.01 specification, the InfoBus 1.2 specification tightens the requirements for event delivery. Section 6.1 of the InfoBus 1.2 specification (see Resources) states: "Data items that implement DataItemChangeManagermust support registration and deregistration of event listeners as per section 6.5.1 of the JavaBeans 1.0 specification. Specifically, changes to the listener list may take place during notification of all listeners. Accordingly, the listener list should be copied at the beginning of a change notification, and the copy of the list used for the duration of the notification."

What are the performance implications of this? In systems such as InfoBus, in which rapid event notification may occur, this scheme of copying and then notifying leads to many short-lived clones that can be a source of performance problems (see "Java Performance Programming, Part 1: Smart Object Management Saves the Day" in Resources). Our goal in this article is to develop a high performance alternative technique for fast listener notification. Our implementation should conform to the InfoBus specification but not produce these short-lived clones.

The basic technique

The key idea behind the technique is that object creation during notification ought to be reduced. By eliminating the clone of the Vectorin the notifyModelChanged() method above, we'd expect to be able to speed up listener notification. Well, why don't we do just that -- eliminate the clone? Unfortunately, doing so would mean that the notifyModelChanged()method would no longer comply with the InfoBus 1.2 specification. To solve this problem, the Modelwill now make a clone in addModelChangedListener()and removeModelChangedListener(), rather than notifyModelChanged(). The Model then uses Java's atomic assignment in addModelChanged(), removeModelChanged(), and notifyModelChanged(); as a result, notify will never see the changes to the notify list once it has created a snapshot reference to it. Finally, we must continue to synchronize the add() and remove() methods, so that they will not encounter any race conditions when called from two different threads.

Here's the sample code with our new technique applied:

public interface ModelChangedListener extends java.util.EventListener { void modelChanged(EventObject e); }

public abstract class Model { private Vector listeners = new Vector(); // list of Listeners public synchronized void addModelChangedListener(ModelChangedListener mcl) { Vector v = (Vector)listeners.clone(); v.addElement(mcl); listeners = v; // Atomic assignment } public synchronized void removeModelChangedListener(ModelChangedListener mcl) { Vector v = (Vector)listeners.clone(); v.removeElement(mcl); listeners = v; // Atomic assignment } protected void notifyModelChanged() { Vector l; EventObject e = new EventObject(this); // Once a reference to the set of EventListeners is acquired // its contents will never change because add and remove modify // a clone of the EventListeners. // This ensures that any changes made to the Vector // from a target listener's method, during the delivery of this // event will not take effect until after the event is delivered l = listeners; // Atomic assignment for (int i = 0; i < l.size(); i++) { // deliver it! ((ModelChangedListener)l.elementAt(i)).modelChanged(e); } } }

The test harness

In order to compare different approaches, we must develop a test-harness framework that can exercise and time different event sources. Each event source needs to implement the TestSourceinterface in order to plug into the test harness. The removeModelChangedListener() method is not included in the interface, because the test harness never calls it; the fireCount() and instanceNumber() properties are used for some of the diagnostic outputs. Let's take a look:

public interface TestSource {
    public void
    addModelChangedListener(ModelChangedListener listener);
    public void notifyModelChanged();
    public int getFireCount();
    public int getInstanceNumber();
}

The test harness uses the event source's class object to create instances of objects that implement the TestSource interface. The test case classes are named Source1 through Source7. The Main method currently exercises Source1 through Source7by passing the class for these sources to the runFor()method. Below is the code for Main:

public class Main {
...
    public static void main(String[] args) {
        try {
...
            runFor(Source1.class);
            runFor(Source2.class);
            runFor(Source3.class);
            runFor(Source4.class);
            runFor(Source5.class);
            runFor(Source6.class);
            runFor(Source7.class);
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }
...

In the code above, the runFor() method runs the test for the class passed as an argument. Instances of the event source are created and then chained together into a simple tree. When the source at the top of the tree fires an event, there is a chain reaction, and all sources in the tree end up firing an event. Thus, the runFor test models environments like InfoBus, in the sense that a change notification from one component may cause other components to deliver change events. To build the tree, the Chain class links two sources; causing source x to deliver an event in turn causes source yto deliver an event. The source code for Chainis shown below:

import java.util.*;

class Chain implements ModelChangedListener { TestSource m_next; Chain(TestSource s1, TestSource s2) { s1.addModelChangedListener(this); m_next = s2; } public void modelChanged(EventObject event) { ... m_next.notifyModelChanged(); } }

The test harness runFor() method creates six instances of the same TestSource, then links them together with instances of the Chain class, as show in the figure below.

Chain of listeners

After chaining the event sources together, the runFor()method calls src.1notifyModelChanged()once to notify src1's listeners. This causes all of the chained listeners to be notified as well. The first call to notifyModelChanged()is not timed, and trace diagnostics are turned on. The diagnostics serve as a crude way to verify that the event source is implemented correctly, and to prime the just-in-time (JIT) compiler by exercising the code once.

Next, the runFor() method records the start time in milliseconds and loops 50,000 times, calling src1.notifyModelChanged()on each pass through the loop. Finally, the runFor() method computes the time taken for the 50,000 calls and prints the results. Here's the code:

static void runFor(Class sourceClass) { try { if (Trace.isOn()) { Trace.out.println(sourceClass.getName()); } TestSource src1 = (TestSource)sourceClass.newInstance(); TestSource src2 = (TestSource)sourceClass.newInstance(); TestSource src3 = (TestSource)sourceClass.newInstance(); TestSource src4 = (TestSource)sourceClass.newInstance(); TestSource src5 = (TestSource)sourceClass.newInstance(); TestSource src6 = (TestSource)sourceClass.newInstance();

Chain c1 = new Chain(src1, src2); Chain c2 = new Chain(src2, src3); Chain c3 = new Chain(src3, src4);

Chain c11 = new Chain(src1, src5); Chain c12 = new Chain(src5, src6); // Test run src1.notifyModelChanged();

// Turn tracing off during timing run Trace.setOn(false);

// Time 50 thousand calls long startTime = System.currentTimeMillis(); for (int i = 0; i < 50000; i++) { src1.notifyModelChanged(); } long stopTime = System.currentTimeMillis();

// Turn tracing back on if (!m_isTableOnly) { Trace.setOn(true); } if (Trace.isOn()) { Trace.out.println(sourceClass.getName() + " time=" + deltaSeconds(stopTime, startTime)); }

System.out.println(deltaSeconds(stopTime, startTime));

if (Trace.isOn()) { Trace.out.println("fireCount=" + src1.getFireCount()); } } catch (Throwable t) { t.printStackTrace(); } }

protected static double deltaSeconds(long stopTime, long startTime) { long deltaMillis = stopTime - startTime; return deltaMillis/1000.0; }

Test cases

OK, now that we have the test harness in place, let's look at some test cases. For the discussion below, the test hardware was a 300 MHz Pentium II machine with 128 MB of memory, running Windows NT 4.0 with Service Pack 5 installed, and using Sun's JDK 1.1.8. The table at the end of this article shows the numbers for the same hardware, OS, and JDK, with additional figures from Sun's JDK 1.3 beta with the HotSpot Client VM and IBM's JDK 1.1.8.

Source1

The first test case is right out of the JavaBeans 1.01 specification shown above. Source1.notifyModelChanged clones the listener vector. I will not repeat the source code here; it is basically the same as the code above, with a few extra methods defined in TestSource to support diagnostics. Fifty thousand notifyModelChanged() calls take, on average, 2.7 seconds. Notice that, because of the chaining of listeners, these 50,000 calls actually cause 300,000 calls to the Source1.notifyModelChanged()method.

Source2

Related:
1 2 3 Page 1