2012-08-24 72 views
2

我正在试图制作一个使用set-in-a-map线程安全的类。我不确定what特别需要同步。Java Collection-Within-Collection并发性

该地图被定义为类似于Map<Class<K>, Set<V>> map;的东西。以下是地图被在实现内部使用的方式减少:

与地图
public void addObject(K key, V object) { 
    getSet(key).add(object); 
} 

public void removeObject(K key, V object) { 
    getSet(key).remove(object); 
} 

public void iterateObjectsInternally(K key, Object... params) 
{ 
    for (V o : getSet(key)) { 
     o.doSomething(params); 
    } 
} 

private Set<V> getSet(K key) { 
    if (!map.containsKey(key)) { 
     map.put(key, new Set<V>()); 
    } 

    return map.get(key); 
} 

问题

至于使用map本身去,唯一的并发问题,我看是在getSet(K),其中线程上下文可以在containsKeyput之间切换。在这种情况下,可能出现以下情况:

[Thread A] map.containsKey(key)  => returns false 
[Thread B] map.containsKey(key)  => returns false 
[Thread B] map.put(key, new Set<V>()) 
[Thread B] map.get(key).add(object) 
[Thread A] map.put(key, new Set<V>()) => Thread A ovewrites Thread B's object [!] 
[Thread B] map.get(key).add(object) 

现在,我目前使用此实现定期HashMap。而且,如果我正确的话,使用Collection.synchronizedMap()ConcurrentHashMap只会解决方法级别的并发问题。也就是说,方法将以原子方式执行。这些都没有提到方法之间相互作用的方式,所以即使使用并行解决方案,以下情况仍然会发生。

ConcurrentHashMap确实有,但是,有方法putIfAbsent。这个缺点是map.putIfAbsent(key, new Set<V>())陈述会在每次请求时创建一个新的集合。这似乎是一个很大的开销。

另一方面,仅仅将这两个语句包装在同步块中就足够了吗?

synchronized(map) { 
    if (!map.containsKey(key)) { 
     map.put(key, new Set<V>()); 
    } 
} 

有没有比锁定整个地图更好的方法?有没有办法只锁定键,以便地图上的其他值的读取不会被锁定?

synchronized(key) { 
    if (!map.containsKey(key)) { 
     map.put(key, new Set<V>()); 
    } 
} 

请注意,密钥不一定是相同的对象(它们是特别Class<?>类型),但是通过哈希码相等。如果同步要求对象地址相等,则同步key可能无法正常工作。

与套装

更大的问题问题,我想,如果是一组被正确使用自知。有几个问题:添加对象,删除对象和迭代对象。

包装Collections.synchronizedList列表是否足以避免addObjectremoveObject中的并发问题?我假设没问题,因为同步封装将使它们成为原子操作。

但是,迭代可能是一个不同的故事。对于iterateObjectsInternally,即使设定是同步的,但它仍然必须保持外部同步:

Set<V> set = getSet(key); 
synchronized(set) { 
    for (V value : set) { 
     // thread-safe iteration 
    } 
} 

然而,这似乎是一个可怕的浪费。如果相反,我们更换简单地使用CopyOnWriteArrayListCopyOnWriteArraySet作为定义。由于迭代只会使用数组内容的快照,因此无法从其他线程修改它。另外,CopyOnWriteArrayList对add和remove方法使用重入锁,这意味着add和remove同样也是安全的(因为它们是同步方法)。 CopyOnWriteArrayList似乎很有吸引力,因为内部结构的迭代次数远远超过列表中修改的次数。此外,使用复制的迭代器,无需担心addObjectremoveObject在另一个线程中搞乱了从iterateObjectInternallyConcurrentModificationExceptions)开始的迭代。

这些并行检查是否在正确的轨道上和/或足够严格?我是一名有并发编程问题的新手,我可能会错过某些明显或过度思考的东西。我知道有几个类似的问题,但是我的实施看起来不同,不能像我那样专门提问。

+0

这是一个_extremely_难题。如果我是你,我会从Guava的'SetMultimap'和['Multimaps.synchronizedSetMultimap']开始(http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common /collect/Multimaps.html#synchronizedSetMultimap(com.google.common.collect.SetMultimap)),它只是锁定每个操作上的所有内容,然后从那里开始工作。 –

回答

1

你肯定是在推翻这一点。根据您的并发特性使用简单的ConcurrentHashMap和ConcurrentSkipListSet/CopyOnWriteArraySet(主要是在迭代需要考虑数据的即时修改时)。使用类似下面的代码片段作为GETSET方法:

private Set<V> getSet(K key) { 
    Set<V> rv = map.get(key); 
    if (rv != null) { 
     return rv; 
    } 
    map.putIfAbsent(key, new Set<V>()); 
    return map.get(key); 
} 

当添加/删除对象,您将需要确定缺少的更新是否是在你的问题域的问题迭代这将确保适当的无锁并发。如果在迭代过程中添加新对象时不会出现问题,请使用CopyOnWriteArraySet。

第三方面,你想深入了解你可以使用什么样的粒度w.r.t.并发性,你的要求是什么,在边缘情况下什么是正确的行为,最重要的是你的代码必须覆盖哪些性能和并发特性 - 如果是在启动时发生两次,我只是让所有的方法同步并成为用它完成。

0

如果您要经常添加到'set',CopyOnWriteArrayList和CopyOnWriteArraySet不会变得可行 - 他们使用太多的资源来添加操作。但是,如果你很少添加,并且经常迭代'set',那么它们就是你最好的选择。

Java ConcurrentHashMap将每个映射放入一个存储桶本身 - 如果缺少操作会在搜索键时锁定列表,然后释放锁并放入键,则放置该存储区。绝对使用ConcurrentHashMap而不是地图。

你的getSet方法本来会很慢,特别是在同步时 - 也许你可以预先加载所有的键和集合。

我建议你按照Louis Wasserman的说法行事,看看你的表现是否符合Guava标准。