我想在“Map of Sets”集合中实现一个变体,它将由多个线程不断访问。我想知道我正在做的同步是否足以保证不会出现任何问题。
所以给定以下代码,其中Map,HashMap和Set是Java实现,Key和Value是一些任意对象:
public class MapOfSets {
private Map<Key, Set<Value>> map;
public MapOfLists() {
map = Collections.synchronizedMap(new HashMap<Key, Set<Value>());
}
//adds value to the set mapped to key
public void add(Key key, Value value) {
Set<Value> old = map.get(key);
//if no previous set exists on this key, create it and add value to it
if(old == null) {
old = new Set<Value>();
old.add(value);
map.put(old);
}
//otherwise simply insert the value to the existing set
else {
old.add(value);
}
}
//similar to add
public void remove(Key key, Value value) {...}
//perform some operation on all elements in the set mapped to key
public void foo(Key key) {
Set<Value> set = map.get(key);
for(Value v : set)
v.bar();
}
}
这里的想法是因为我已经同步了Map本身,所以get()和put()方法应该是原子的吗?因此,不需要在Map或其中包含的集合上执行其他同步。这样会有用吗?
或者,上述代码是否优于另一种可能的同步解决方案:
public class MapOfSets {
private Map<Key, Set<Value>> map;
public MapOfLists() {
map = new HashMap<Key, Set<Value>();
}
public synchronized void add(Key key, Value value) {
Set<Value> old = map.get(key);
//if no previous set exists on this key, create it and add value to it
if(old == null) {
old = new Set<Value>();
old.add(value);
map.put(old);
}
//otherwise simply insert the value to the existing set
else {
old.add(value);
}
}
//similar to add
public synchronized void remove(Key key, Value value) {...}
//perform some operation on all elements in the set mapped to key
public synchronized void foo(Key key) {
Set<Value> set = map.get(key);
for(Value v : set)
v.bar();
}
}
我将数据结构保持不同步但同步所有可能的公共方法。那些哪些会起作用,哪一个更好?
答案 0 :(得分:3)
第二个是正确的,但第一个不是。
考虑一下,并假设两个线程并行调用add()
。这是可能发生的事情:
在故事的最后,地图包含一个“foo”而不是两个值。
同步地图可确保其内部状态一致,并且您在地图上调用的每个方法都是线程安全的。但它并没有使得get-then-put操作成为原子。
考虑使用Guava的SetMultiMap实现之一,它可以为您完成所有工作。将其包装到Multimaps.synchronizedSetMultimap(SetMultimap)
的调用中以使其成为线程安全的。
答案 1 :(得分:3)
您发布的第一个实现不是线程安全的。考虑当具有相同add
的两个并发线程访问key
方法时会发生什么:
null
引用,因为没有包含给定键的项目null
引用,因为没有包含给定键的项目 - 这将在 A 之后发生在第一次调用时返回,因为地图已同步false
false
从那时起,两个线程将继续执行if语句的true
分支,并且您将丢失两个value
对象中的一个。
您发布的方法的第二个变体看起来更安全。
但是,如果您可以使用第三方库,我建议您查看Google Guava,因为它们提供并发的多重映射(docs)。
答案 2 :(得分:3)
您的第二个实现将起作用,但它保持锁定的时间超过了它所需的时间(使用同步方法而不是同步块的不可避免的问题),这将减少并发性。如果您发现此处的并发限制是瓶颈,则可以缩小锁定区域。
或者,您可以使用java.util.concurrent
提供的一些无锁集合。这是我的尝试;这没有经过测试,它要求Key具有可比性,但它不应该执行任何锁定:
public class MapOfSets {
private final ConcurrentMap<Key, Set<Value>> map;
public MapOfSets() {
map = new ConcurrentSkipListMap<Key, Set<Value>>();
}
private static ThreadLocal<Set<Value>> freshSets = new ThreadLocal<Set<Value>>() {
@Override
protected Set<Value> initialValue() {
return new ConcurrentSkipListSet<Value>();
}
};
public void add(Key key, Value value) {
Set<Value> freshSet = freshSets.get();
Set<Value> set = map.putIfAbsent(key, freshSet);
if (set == null) {
set = freshSet;
freshSets.remove();
}
set.add(value);
}
public void remove(Key key, Value value) {
Set<Value> set = map.get(key);
if (set != null) {
set.remove(value);
}
}
//perform some operation on all elements in the set mapped to key
public void foo(Key key) {
Set<Value> set = map.get(key);
if (set != null) {
for (Value v: set) {
v.bar();
}
}
}
}
答案 3 :(得分:0)
对于Map实现,您可以使用ConcurrentHashMap - 您无需担心确保访问的线程安全性,无论是输入还是检索,因为实现会为您处理。
如果您真的想使用Set,可以调用
Collections.newSetFromMap(new ConcurrentHashMap<Object,Boolean>())
在ConcurrentHashMap上。