Skip to main content

Good advice for coding listeners

Posted by emcmanus on July 29, 2005 at 4:01 AM PDT

The consistently excellent Brian Goetz has written a new article in his Java Theory and Practice series entitled "Be a good (event) listener". Since listeners are an important part of the JMX API, here's how his advice applies there.

There are really two sets of recommendations, one for event generators and one for event listeners.

In the JMX API, events are instances of
Notification
, event generators are instances of
NotificationEmitter
(or its ancestor NotificationBroadcaster), and event listeners are instances of
NotificationListener
.

The advice for event generators (NotificationEmitters) can be summarized as:

  • Be careful about concurrency between, on the one hand, sending an event to the list of listeners, and on the other hand, changing this list.
  • Ensure that if a listener throws a RuntimeException, that doesn't stop other listeners from getting the event.

Fortunately, the standard implementation class
NotificationBroadcasterSupport
already follows this advice. It uses a
CopyOnWriteArrayList
to manage the list of listeners. It catches a RuntimeException from any listener and does not let it affect later listeners or the sender of the event.

If you have an MBean that sends notifications, it is almost always best to use NotificationBroadcasterSupport. In the simplest case, you can simply extend this class. Since the Java language doesn't support multiple inheritance, sometimes you can't do this because you are already extending another class. In that case you can still use delegation, like this:

public class Something extends SomethingElse
        implements SomethingMBean, NotificationEmitter {

    private final NotificationBroadcasterSupport nbs =
        new NotificationBroadcasterSupport();
    private static final MBeanNotificationInfo[] notificationInfo = {...};

    public void addNotificationListener(NotificationListener nl,
                                        NotificationFilter nf,
                                        Object handback) {
        nbs.addNotificationListener(nl, nf, handback);
    }

    public MBeanNotificationInfo[] getNotificationInfo() {
        return notificationInfo.clone();
    }

    ...the two removeNotificationListener methods delegate like
       addNotificationListener...

    ...code that wants to send a notification does:
        Notification n = ...;
        nbs.sendNotification(n);
}

In Java SE 6 (Mustang), NotificationBroadcasterSupport will acquire a
new constructor
that allows you to specify the MBeanNotificationInfo[] array directly, so you can delegate the getNotificationInfo() method too.

Concerning event listeners, Brian Goetz has four pieces of advice:

  1. The listener shouldn't add itself from its own constructor.
  2. Ensure that you always remove listeners when they are no longer relevant.
  3. Listeners usually run in a separate thread so pay attention to thread safety.
  4. Listeners should not perform blocking operations.

I haven't noticed people doing the first thing in this list, but if you have, now you know you should stop.

The second is very good advice, and in particular you should pay attention to exceptions. You should be sure that every addNotificationListener is always matched by a removeNotificationListener no matter what execution path the code follows.

Paying attention to thread safety is just as important for listeners in the JMX API as elsewhere.

Finally, listeners should not block. If a local listener blocks, meaning a listener that is in the same Java Virtual Machine as the MBean sending the notification, then that will usually cause the sending MBean to block too until the listener completes. If a remote listener blocks, it won't hold up the sending MBean, but it will prevent the delivery of any other notifications from the same connection until it finishes.

If you need to do a blocking operation, you won't want to use the
invokeLater
facility mentioned by Brian Goetz unless you are in a graphical application. The facilities from
java.util.concurrent
provide a good alternative. For example, you could change this listener...

class BlockingListener implements NotificationListener {
    public void handleNotification(Notification n, Object handback) {
        blockingOperation();
    }
}

...into this one...

class NonBlockingListener implements NotificationListener {
    private final Executor executor =
        Executors.newSingleThreadExecutor();

    public void handleNotification(Notification n, Object handback) {
        executor.execute(new Runnable() {
            public void run() {
                blockingOperation();
            }
        });
    }
}

This creates a private thread that will handle the call to blockingOperation(). If a second notification arrives while the thread is still doing the blockingOperation() of the first, then the second blockingOperation() will be queued up and executed when the first has finished.

If you have many listeners where you need to do blocking operations like this, you might consider sharing a newSingleThreadExecutor between them, for example with a static field that they all access. You might also want to have a pool of threads rather than a single thread for the blocking operations; java.util.concurrent has everything you need for that.

Finally, if you have an MBean that absolutely must not block when it sends a notification, even in the face of a badly-behaved listener that does block, then Mustang provides another
new constructor
for NotificationBroadcasterSupport that allows you to specify an Executor to be used for calling listeners. Again you can use the facilities of java.util.concurrent to control how this works. You might want to think about the worst case: a listener that never returns. You don't want this to always cause a new thread to be created for every notification, and you don't want it to always cause a new entry to be added to a queue of tasks for every notification either. So you're going to need to throw notifications away sometimes. You can achieve this with a
ThreadPoolExecutor
that has a bounded queue, a bounded number of threads, and a DiscardPolicy or DiscardOldestPolicy.

Related Topics >>