4

I am always very hesitant to bring my locks in the open, to make them public. I always try to keep the locks restricted to my implementation. Not doing that, is a recipe for deadlocks, I believe.

I have the following class:

class SomeClass {
    protected ArrayList<Listener> mListeners = new ArrayList<Listener>();

    protected void addListener(Listener listener) {
        synchronized (mListeners) {
            mListeners.add(listener);
        }
    }

    protected void removeListener(Listener listener) {
        synchronized (mListeners) {
            mListeners.remove(listener);
        }
    }

    ...
}

When SomeClass wants to notify his listeners, would you do:

    synchronized (mListeners) {
        for (Listener l : mListeners) {
             l.event();
        }
    }

or

    Listener[] listeners = null;

    synchronized (mListeners) {
        listeners = mListeners.toArray();
    }
    for (Listener l : listeners) {
        l.event();
    }

I would choose the second option. The downside is that listeners can get events, even though they are already unregistered. The upside is that a thread, on which a listener calllback is waiting, cannot run into a deadlock when he wants to unregister a listener. I believe the upside is way more important than the downside, which can be easily documented.

So the question here is basically: would you expose your lock, or not?

My question is NOT if you would choose a plain ArrayList, a LinkedList, a ConcurrentLinkedQueue, a CopyOnWriteArrayList, a ...! It is whether you would mind if a listener can get a notification while it is already unregistered. It is whether you would bring the lock in the open, or not. It's about avoiding deadlocks, or not.

Please share your thoughts. Thanks!

Japer D.
  • 754
  • 1
  • 9
  • 18
  • use [synchronizedList](http://docs.oracle.com/javase/6/docs/api/java/util/Collections.html#synchronizedList(java.util.List)). For notifications, the synchronized block around the loop is good. – aishwarya Nov 24 '11 at 15:48
  • 2
    The `CopyOnWriteArrayList` is appropriate for situations when traversal is much more common than updating. – artbristol Nov 24 '11 at 15:52
  • Please read my edit. It's about the lock, not about the container. – Japer D. Nov 24 '11 at 15:57
  • Writing this by hand when the j.u.c toolbox is to hand is asking for trouble. The toolkit provides not only code, but also standard behavioral conventions that are likely to be familiar to your clients. – kittylyst Nov 24 '11 at 16:09
  • -1 You're asking a loaded question. You're assuming that you need to use a lock and asking if it should be exposed. The proper answer is that you shouldn't need a lock, and several options to do this have been presented. – Erick Robertson Nov 24 '11 at 16:24
  • @ErickRobertson No he's not. It's a perfectly fine question. – Michael Nov 24 '11 at 16:36
  • Okay, I'm talking to @JaperD in chat, and he basically wanted to ask a different question. If you want to answer this one, or close it, whatever. It's too late to change this question, I feel. http://chat.stackoverflow.com/transcript/message/1946316#1946316 – Erick Robertson Nov 24 '11 at 16:37
  • Sorry, it's my English! I indeed wanted to ask a different question. I did not want to focus on the data structure, but on the idea of getting events while already being unregistered. I tried again on http://stackoverflow.com/questions/8260205/when-a-listener-is-removed-is-it-okay-that-the-event-be-called-on-that-listener . – Japer D. Nov 24 '11 at 16:44

4 Answers4

7

Use a CopyOnWriteArrayList for your listener arrays.

This is perfect for listener arrays which change infrequently. When you iterate through them, you're iterating through the underlying array. With a CopyOnWriteArrayList, this array is copied every time it is modified. So there is no need to synchronize with it when iterating because each underlying array is guaranteed to be static, even past its use within the CopyOnWriteArrayList.

Since CopyOnWriteArrayList is also thread safe, you do not need to synchronize the add & remove operations.

Declaration:

private final CopyOnWriteArrayList<Listener> listeners;

Event trigger:

for (Listener l: this.listeners) {
  l.event();
}
Erick Robertson
  • 32,125
  • 13
  • 69
  • 98
  • If it's vital that you never get another event call once the listener has been removed, then you'll either need to use the first code sample or set a flag in the listener to ignore the event. But if you do this, it really complicates your addListener and removeListener methods, because you will have to detect if this method is being called from within your event() method on a listener. If you use the `CopyOnWriteArrayList`, then you don't have to synchronize when iterating. This invalidates your question. – Erick Robertson Nov 24 '11 at 16:20
  • Thanks, makes sense. By the way, CopyOnWriteArrayList = my second option, conceptually wise. – Japer D. Nov 24 '11 at 16:48
  • 1
    @ErickRobertson "Note you should still synchronize the add & remove operations." I would have thought not. Can you explain? – assylias Mar 28 '12 at 08:00
  • 1
    @assylias I stand corrected. `CopyOnWriteArrayList` is thread-safe, so there is no need to synchronize. I will update my answer. Thanks! – Erick Robertson Mar 28 '12 at 15:59
2

I would use a ConcurrentLinkedQueue<Listener> which is made for this kind of problems: adding, removing and iterating simultaneously on a collection.

A precision : this solution prevents a listener from being called from the very moment it is deregistered. This solution has the best accuracy, the finest granularity, and is probably the solution that is the less deadlock-prone.

If you're adamant about your two proposals, I would choose the first one because it is more secure, but it's likely to provoke longer locks and reduce overall performance (well it depends on how often you add or remove listeners). The second solution is broken because when a listener deregisters itself, it might well be because he's become unable to handle events. In such a case, calling it would be a very bad idea, and in any case it would be a violation of the listener contract.

solendil
  • 8,432
  • 4
  • 28
  • 29
  • For my question, that's just syntax sugar. The question here is whether you would notify the listeners outside or inside the synchronization lock. – Japer D. Nov 24 '11 at 15:44
  • 1
    Not sugar, because in this case there's no lock: the queue has atomic locks on add and remove and the iterator remains consistant. It means that you start iterating, an element is added, it will be called, another is removed, it won't, all this in the same iteration cycle. It's the best of both world: ensuring synchronization, while being fine grained on who gets called and who's not. – solendil Nov 24 '11 at 15:46
  • I see. That solution is the same as my second option. There, a listener can get a notification while it might already been unregistered. – Japer D. Nov 24 '11 at 15:53
  • No again :) Because if your listener deregister itself while the loop is being done and the loop has not yet notified it, it will never be notified. Another way to put it: the granularity of your code is collection-wide (Q is whether to record collection before notifying (fast, innacurate) or preventing collection modif while notifying (slow because of collection-wide lock)). The Concurrent stuff has a listener granularity : you don't lock your collection, yet you take collection modifications into account while your loop is iterating ! – solendil Nov 24 '11 at 16:04
  • Updated my answer with an analysis of the two solutions you propose. – solendil Nov 24 '11 at 16:12
  • I get it now solendil. Thanks, this answer is spot on! – Japer D. Nov 24 '11 at 16:19
  • Note that this is a great answer if you're adding and removing listeners constantly. Typically a listener is being added at startup and removed rarely. In this case, the full thread protections of `ConcurrentLinkedQueue` are not required and a `CopyOnWriteArrayList` is appropriate. – Erick Robertson Nov 24 '11 at 16:40
1

I guess I would choose the second option too. Like I think you said, the second option doesn't hold the lock when it's notifying the listeners. So, if one of the listeners takes a long time to do its thing, other threads can still call the addListener or removeListener methods without having to wait for the lock to be released.

Michael
  • 34,873
  • 17
  • 75
  • 109
  • So it's up to the listener implementations to filter late-coming notifications? – Japer D. Nov 24 '11 at 16:00
  • 1
    @Japer D. So the problem is: if an event causes the listeners to fire, what happens if a thread wants to add or remove a listener while it's iterating through the list of listeners? I would just say: tough luck. The change that was made to the list of listeners won't be "recognized" until the next time an event occurs. I mean, I don't know how else to handle it. – Michael Nov 24 '11 at 16:05
  • No, it's more simple than that. If a thread calls removeListener(himself), he still can get a notificaiton, since the copy of the listeners might have been made before, in the second option. In the first option, that's not possible, but you risk introducing deadlocks since you expose the lock. – Japer D. Nov 24 '11 at 16:07
  • @JaperD. Exactly, I agree. :) But I don't think that's a big deal (getting one more notification after calling removeListener(himself)) – Michael Nov 24 '11 at 16:10
1

there are several datastructures available that allow concurrent adding, removing and iterating

a ConcurrentLinkedQueue is fully lockless and fast on add and remove (bar the O(n) traversel to find it) and add, but there may be some interference of other threads (a bulk remove may only be partially visible to the iterator)

the copyOnWrite list and set are slower on add and remove because they entail a array allocation and copy, however the iteration is completely free of interference and as fast as traversing a ArrayList of the same size (the iteration happens on a snapshot of the set)

you can build a ConcurrentHashSet on the ConcurrentHashMap but this has the same (usefull)properties besides a fast O(1) remove and the locks used for adding and removing

ratchet freak
  • 47,288
  • 5
  • 68
  • 106