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