我是否应该同步监听器通知?

发布于 2024-12-17 20:47:07 字数 1198 浏览 4 评论 0原文

我总是很犹豫是否要把我的锁公开,公开。我总是尝试将锁限制在我的实现范围内。我相信,不这样做就会导致僵局。

我有以下课程:

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);
        }
    }

    ...
}

当 SomeClass 想要通知他的听众时,你会这样做吗:

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

或者

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

我会选择第二个选项。缺点是监听器可以获取事件,即使它们已经未注册。好处是,等待侦听器回调的线程在想要取消注册侦听器时不会陷入死锁。我相信好处比坏处更重要,这很容易记录下来。

所以这里的问题基本上是:你会暴露你的锁吗?

我的问题不是你是否会选择一个普通的 ArrayList、一个 LinkedList、一个 ConcurrentLinkedQueue、一个 CopyOnWriteArrayList、一个...!这是您是否介意侦听器在未注册时可以获得通知。问题在于你是否愿意把锁打开。这是为了避免僵局,或者不是。

请分享您的想法。谢谢!

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!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

岛歌少女 2024-12-24 20:47:07

对侦听器数组使用CopyOnWriteArrayList

这非常适合不经常更改的侦听器数组。当您迭代它们时,您正在迭代底层数组。使用CopyOnWriteArrayList,每次修改该数组时都会复制该数组。因此,迭代时无需与其同步,因为每个底层数组都保证是静态的,即使在 CopyOnWriteArrayList 中使用它也是如此。

由于 CopyOnWriteArrayList 也是线程安全的,因此您不需要同步 add & 操作。删除操作。

声明:

private final CopyOnWriteArrayList<Listener> listeners;

事件触发:

for (Listener l: this.listeners) {
  l.event();
}

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();
}
提笔书几行 2024-12-24 20:47:07

我想我也会选择第二个选项。就像我认为你说的那样,第二个选项在通知听众时不会保持锁定。因此,如果其中一个侦听器需要很长时间才能完成其操作,其他线程仍然可以调用 addListenerremoveListener 方法,而无需等待锁被释放。

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.

半枫 2024-12-24 20:47:07

有几种可用的数据结构允许并发添加、删除和迭代

ConcurrentLinkedQueue 是完全无锁的,并且添加和删除速度很快(禁止 O(n) 遍历来找到它)和添加,但可能存在其他线程的一些干扰(批量删除可能只是部分可见)到迭代器)

copyOnWrite 列表set 在 add 和删除,因为它们需要数组分配和复制,但是迭代完全不受干扰,并且与遍历相同大小的 ArrayList 一样快(迭代发生在集合的快照上),

您可以在 ConcurrentHashMap但是除了快速 O(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

浪推晚风 2024-12-24 20:47:07

我会使用 ConcurrentLinkedQueue是为此类问题而设计的:在集合上同时添加、删除和迭代。

精度:此解决方案可防止侦听器从注销的那一刻起就被调用。该解决方案具有最好的准确性、最细的粒度,并且可能是最不易出现死锁的解决方案。

如果您坚持您的两个建议,我会选择第一个,因为它更安全,但它可能会引发更长的锁定并降低整体性能(这取决于您添加或删除侦听器的频率)。第二个解决方案已被破坏,因为当侦听器自行注销时,很可能是因为他无法处理事件。在这种情况下,调用它将是一个非常糟糕的主意,并且无论如何这都会违反监听器契约。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文