2016-09-13 71 views
0

我有一个Java 1.8类,它拥有两个集合:同步方法,以防止ConcurrentModificationException的

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异常。我在想同步的“这样”的所有方法:

set.add(object); 

将成为

synchronized(this){ 
    set.add(object); 
} 

,我会增加一个类似声明同步到其他4种方法,包括环法。

这项工作?我知道同步方法可能会导致瓶颈。尽管目前没有性能问题,但我希望将此设计合理化并听取可能的性能下降较少的替代方案。

+1

@AndyTurner这可能不会解决问题 - Collections.synchronizedSet'在迭代期间没有锁定,所以仍然可以很容易地获得'ConcurrentModificationException'。 – Sbodd

回答

1

因为锁是可重入的,所以同步对集合的访问不会停止迭代集合的同一线程,然后修改集合本身。尽管名称不同,ConcurrentModificationException并不总是由于另一个线程的并发修改而引发。在通知回调中注册或取消注册其他观察者是此类并发修改的主要罪魁祸首。

触发事件的常用策略是在发送任何事件之前先通知侦听器的“快照”。因为观察者通知的顺序通常是未指定的,所以对于所有在接收到事件时注册的观察者都是有效的,即使另一个观察者由于该通知而取消注册。

要进行快照,可以观察员复制到一个临时集合或数组:

Collection<?> observers = new ArrayList<>(mySet); 

Object[] observers = mySet.toArray(new Object[mySet.size()]); 

然后在该副本迭代,离开原来的可供更新:

for (Object o : observers) { 
    ... 
    doSomething(o, ...); 
    ... 
} 

某些并发集合如ConcurrentSkipListSet不会引发异常,但它们只能保证“弱一致”迭代。这可能会导致一些(新增加的)观察者意外地收到当前通知。 CopyOnWriteArraySet内部使用快照技术。如果你很少修改这个集合,它会更有效率,因为它只在必要时复制数组。

+0

[这篇文章有一些很好的例子](http://stackoverflow.com/a/28930647/3474)弱一致的迭代。 – erickson

+0

我理解为什么一个线程理论上可以同时访问add/remove方法和循环方法,但我不明白如何在代码上顺序运行的一个线程可以同时执行这两个线程。就像,当它添加一个对象时,它不能循环,因为它是一个单独的线程。一个线程可以被cpu/cores“分割”成同时执行不同的任务吗? – user1884155

+0

@ user1884155不,这并不复杂。这只是一个修改集合的循环。像你最初的例子一样,假设你的'doSomething()'方法将一个元素添加到'mySet'中。这可能会引发'ConcurrentModificationException'。线程一次只执行一个动作:检查是否有另一个元素;如果是这样,就推进到它;然后处理它;然后重复。问题是,如果“处理它”导致修改集合,那么在下一次重复时发生的“检查以查看是否存在另一个元素”会检测集合已更改并引发异常。 – erickson

0

一个可能会比只使用​​块更好的答案是将您的集合更改为线程安全的集合。而不是HashMap(或您正在使用的任何地图),请使用ConcurrentHashMap。将Set替换为CopyOnWriteArraySetConcurrentSkipListSet。您需要阅读这些文档以确保它们的线程行为符合您的实际需求,但两者都应该是对线程不安全的类进行改进而不进行同步。

2

不,它不安全,除非循环同步。如果要避免每个循环持续时间的锁定开销,请考虑使用并发集合,如ConcurrentHashMapCollections.newSetFromMap(new ConcurrentHashMap<>())

+0

你是否说并发散列图也锁定在遍历该映射的键集上? – user1884155

+0

@ user1884155它不锁定,但它保证是线程安全的。详情请参阅文档。 – shmosel

+0

我应该如何在文档中解释这条消息:“他们不会抛出ConcurrentModificationException,但是迭代器一次只能被一个线程使用。”他们的意思是“旨在”?如果我删除循环方法中的同步语句,但使用concurrenthashmap,如果两个线程同时开始迭代会发生什么? – user1884155