Java Collection-Within-Collection并发

时间:2012-08-24 00:01:10

标签: java collections concurrency

我正在尝试创建一个利用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),其中线程上下文可能在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同步可能不起作用。

Set

的问题

我认为更大的问题是知道该套装是否正确使用。存在一些问题:添加对象,删除对象和迭代对象。

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

然而,迭代可能是一个不同的故事。对于iterateObjectsInternally,即使集合已同步,它仍然必须在外部同步:

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

然而,这似乎是一种可怕的浪费。相反,如果我们替换,只需使用CopyOnWriteArrayListCopyOnWriteArraySet作为定义。由于迭代只是使用数组内容的快照,因此无法从另一个线程修改它。此外,CopyOnWriteArrayList在添加和删除方法上使用可重入锁,这意味着添加/删除本身也是安全的(因为它们是同步方法)。 CopyOnWriteArrayList似乎很有吸引力,因为内部结构上的迭代次数远远超过列表中的修改次数。此外,使用复制的迭代器,无需担心addObjectremoveObject在另一个线程中弄乱iterateObjectInternallyConcurrentModificationExceptions)的迭代。

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

2 个答案:

答案 0 :(得分: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可以使用哪种粒度。并发性,你的要求是什么,边缘情况下的正确行为是什么,最重要的是,你的代码必须涵盖哪些性能和并发特性 - 如果它在启动时发生两次,我只是使所有方法同步并且是完成它。

答案 1 :(得分:0)

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

Java ConcurrentHashMap将每个映射本身放入存储桶中 - 如果缺少操作,则在搜索密钥时锁定列表,然后释放锁并放入密钥。绝对使用ConcurrentHashMap而不是map。

你的getSet方法可能本来就很慢,特别是在同步时 - 也许你可以提前加载所有的密钥和集合。

我建议你按照Louis Wasserman的说法进行操作,看看你的表现是否与Guava实施相当。