【问题标题】:Java Collection-Within-Collection ConcurrencyJava 集合内集合并发
【发布时间】:2012-08-24 00:01:09
【问题描述】:

我正在尝试创建一个使用 set-within-a-map 线程安全的类。我不确定what 特别需要同步。

映射定义为类似于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),其中线程上下文可能在containsKey 和put 之间切换。在这种情况下,可能会发生以下情况:

[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&lt;V&gt;()) 将在每次请求集合时创建一个新集合。这似乎是很多开销。

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

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&lt;?&gt; 类型),但在哈希码上是相等的。如果同步需要对象地址相等,则key 同步可能不起作用。

集合的问题

我认为,更大的问题是知道这套设备是否被正确使用。有几个问题:添加对象、删除对象和迭代对象。

将列表包装在Collections.synchronizedList 中是否足以避免addObject 和removeObject 中的并发问题?我假设这会很好,因为同步包装器会使它们成为原子操作。

但是,迭代可能是另一回事。对于iterateObjectsInternally,即使set是同步的,还是要对外同步:

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

但是,这似乎是一种可怕的浪费。相反,如果我们替换简单地使用 CopyOnWriteArrayList 或 CopyOnWriteArraySet 作为定义。由于迭代只会使用数组内容的快照,因此无法从另一个线程对其进行修改。此外,CopyOnWriteArrayList 在 add 和 remove 方法上使用了可重入锁,这意味着 add/remove 本质上也是安全的(因为它们是同步方法)。 CopyOnWriteArrayList 似乎很有吸引力,因为内部结构的迭代次数远远超过列表上的修改次数。此外,使用复制的迭代器,无需担心addObject 或removeObject 会在另一个线程中将iterateObjectInternally (ConcurrentModificationExceptions) 的迭代搞乱。

这些并发检查是否在正确的轨道上和/或足够严格?我是一个有并发编程问题的新手,我可能遗漏了一些明显的或过度思考的东西。我知道有一些类似的问题,但我的实现似乎足够不同,足以保证像我一样具体地提出问题。

【问题讨论】:

  • 这是一个非常的难题。如果我是你,我会从 Guava 的 SetMultimap 和 Multimaps.synchronizedSetMultimap 开始,它们只是锁定每个操作的所有内容,然后从那里开始工作。

标签: java collections concurrency


【解决方案1】:

你肯定想多了。根据您的并发特征使用简单的 ConcurrentHashMap 和 ConcurrentSkipListSet/CopyOnWriteArraySet(主要是如果迭代需要考虑数据的动态修改)。使用类似于以下 sn-p 的方法作为 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.并发性,您的要求是什么,边缘情况下的正确行为是什么,最重要的是,您的代码必须涵盖哪些性能和并发特征 - 如果它在启动时发生两次,我只会让所有方法同步并成为完成它。

【讨论】:

    【解决方案2】:

    如果您要经常添加到“集合”,CopyOnWriteArrayList 和 CopyOnWriteArraySet 将不可行 - 它们使用太多资源进行添加操作。但是,如果您很少添加,并且经常迭代“集合”,那么它们是您最好的选择。

    Java ConcurrentHashMap 将每个映射本身放入一个存储桶中——如果不存在,您的 put if 操作将在它搜索键时锁定列表,然后释放锁并放入键。绝对使用 ConcurrentHashMap 而不是 map。

    您的 getSet 方法本身可能会很慢,尤其是在同步时 - 也许您可以尽早预加载所有键和集合。

    我建议你按照 Louis Wasserman 所说的去做,看看你在 Guava 实现中的表现是否不错。

    【讨论】:

      猜你喜欢
      • 2012-03-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-10-20
      • 1970-01-01
      • 1970-01-01
      • 2021-07-10
      • 1970-01-01
      相关资源
      最近更新 更多