我总是非常犹豫要把我的锁公开,让它们公开。我总是试图将锁限制在我的实现中。我不相信,这不是一个死锁的秘诀。
我有以下课程:
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();
}
我会选择第二种选择。缺点是听众可以获得活动,即使他们已经取消注册。好处是,一个侦听器calllback正在等待的线程,当他想取消注册一个监听器时,它不会遇到死锁。我认为好处比下行更重要,可以很容易地记录下来。
所以这里的问题基本上是:你会暴露你的锁吗?
我的问题不是你选择一个简单的ArrayList,LinkedList,ConcurrentLinkedQueue,CopyOnWriteArrayList,......!您是否会介意监听器是否可以在未注册时收到通知。无论你是否将锁打开,或不是。这是关于避免死锁。
请分享您的想法。谢谢!
答案 0 :(得分:6)
为监听器阵列使用CopyOnWriteArrayList
。
这对于不经常更改的侦听器阵列非常适合。当您遍历它们时,您将遍历底层数组。使用CopyOnWriteArrayList
时,每次修改此数组时都会复制该数组。所以在迭代时不需要与它同步,因为每个底层数组都保证是静态的,甚至超过它在CopyOnWriteArrayList
中的使用。
由于CopyOnWriteArrayList
也是线程安全的,因此您无需同步add&amp; amp;删除操作。
声明:
private final CopyOnWriteArrayList<Listener> listeners;
事件触发器:
for (Listener l: this.listeners) {
l.event();
}
答案 1 :(得分:2)
我会使用ConcurrentLinkedQueue<Listener>
来解决这类问题:在集合上同时添加,删除和迭代。
精度:此解决方案可防止在取消注册时调用侦听器。此解决方案具有最佳的准确性,最精细的粒度,并且可能是解决方案,更容易出现死锁。
如果你坚持你的两个提议,我会选择第一个,因为它更安全,但它可能会引发更长的锁并降低整体性能(这取决于你添加或删除监听器的频率)。第二种解决方案被打破了,因为当听众退出注册时,很可能是因为他无法处理事件。在这种情况下,调用它将是一个非常糟糕的主意,并且无论如何它将违反听众合同。
答案 2 :(得分:1)
我想我也会选择第二个选项。就像我想你说的那样,第二个选项在通知听众时没有锁定。因此,如果其中一个侦听器需要很长时间才能执行此操作,则其他线程仍可以调用addListener
或removeListener
方法,而无需等待锁定被释放。
答案 3 :(得分:0)
有几种可用的数据结构允许并发添加,删除和迭代
ConcurrentLinkedQueue是完全无锁且快速添加和删除(禁止O(n)遍历以找到它)并添加,但可能存在一些其他线程的干扰(批量删除可能只是部分对迭代器可见
copyOnWrite list和set在添加和删除时速度较慢,因为它们需要进行数组分配和复制,但迭代完全没有干扰,并且与遍历相同大小的ArrayList一样快(迭代发生在集合的快照上)
您可以在ConcurrentHashMap上构建ConcurrentHashSet,但除了快速O(1)删除和用于添加和删除的锁
之外,它还具有相同的(有用的)属性