我有一个Java 1.8类,它包含两个集合:
Map<Key,Object>
Set<Object>
我的五种方法:
addObjectToMap()
removeObjectFromMap()
addObjectToSet()
removeObjectFromSet()
loopOverEverything(){
for(Object o : mySet){
for(Object o2 : myMap.getKeySet()){
doSomething(o,o2);
}
}
}
该类的目的是实现观察者模式,但在观察者和观察者中都非常灵活。我遇到的问题是,当循环进行循环时线程调用add / remove方法时,最后一个方法很容易抛出ConcurrentModificationException。我正在考虑同步所有方法&#34;这&#34;:
set.add(object);
会变成
synchronized(this){
set.add(object);
}
我会向其他4个方法添加一个类似的synchronize语句,包括循环方法。
这会有用吗?我知道同步方法可能会导致瓶颈。即使目前没有性能问题,我也希望对此设计进行合理化,并了解可能性能较差的替代方案。
答案 0 :(得分:2)
不,除非循环也同步,否则它不安全。如果要在每个循环的持续时间内避免锁定开销,请考虑使用并发集合,例如ConcurrentHashMap
和Collections.newSetFromMap(new ConcurrentHashMap<>())
。
答案 1 :(得分:1)
因为锁是可重入的,所以同步对集合的访问不会停止迭代集合的同一线程,然后修改集合本身。尽管名称如此,但由于另一个线程的并发修改,并不总是引发ConcurrentModificationException
。在通知回调中注册或取消注册其他观察者是这种并发修改的罪魁祸首。
触发事件的常见策略是在发送任何事件之前获取要通知的侦听器的“快照”。由于通知观察者的顺序通常是未指定的,因此对于在生成事件时接收它的所有观察者都有效,即使另一个观察者因该通知而取消注册它。
要制作快照,您可以将观察者复制到临时集合或数组:
Collection<?> observers = new ArrayList<>(mySet);
或
Object[] observers = mySet.toArray(new Object[mySet.size()]);
然后遍历该副本,让原始版本可用于更新:
for (Object o : observers) {
...
doSomething(o, ...);
...
}
某些并发集合(如ConcurrentSkipListSet
)不会引发异常,但它们只能保证“弱一致”迭代。这可能导致一些(新添加的)观察者意外地接收到当前通知。 CopyOnWriteArraySet
在内部使用快照技术。如果你很少修改集合,它会更有效率,因为它只在必要时复制数组。
答案 2 :(得分:0)
一个可能比仅使用synchronized
块更好的答案是将集合更改为线程安全的集合。使用HashMap
代替ConcurrentHashMap
(或您正在使用的任何地图)。将Set
替换为CopyOnWriteArraySet
或ConcurrentSkipListSet
。您需要阅读有关这些文档的文档,以确保它们的线程行为符合您的实际需要,但两者都应该是对不安全线程的类的改进。