同步集/列表的映射

时间:2012-03-04 09:51:05

标签: java multithreading collections concurrency synchronization

我想在“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();
    }
}

我将数据结构保持不同步但同步所有可能的公共方法。那些哪些会起作用,哪一个更好?

4 个答案:

答案 0 :(得分:3)

第二个是正确的,但第一个不是。

考虑一下,并假设两个线程并行调用add()。这是可能发生的事情:

  • 线程1调用add(“foo”,bar“);
  • 线程2调用add(“foo”,baz“);
  • 线程1获取“foo”的设置:null
  • 线程2获取“foo”:null
  • 的设置
  • 线程1创建一个新集并在其中添加“bar”
  • 线程2创建一个新集合并在其中添加“baz”
  • 线程1将其设置放在地图中
  • 线程2将其设置放在地图中

在故事的最后,地图包含一个“foo”而不是两个值。

同步地图可确保其内部状态一致,并且您在地图上调用的每个方法都是线程安全的。但它并没有使得get-then-put操作成为原子。

考虑使用Guava的SetMultiMap实现之一,它可以为您完成所有工作。将其包装到Multimaps.synchronizedSetMultimap(SetMultimap)的调用中以使其成为线程安全的。

答案 1 :(得分:3)

您发布的第一个实现不是线程安全的。考虑当具有相同add的两个并发线程访问key方法时会发生什么:

  1. 主题 A 执行方法的第1行,并获得null引用,因为没有包含给定键的项目
  2. 主题 B 执行方法的第1行,并获得null引用,因为没有包含给定键的项目 - 这将在 A 之后发生在第一次调用时返回,因为地图已同步
  3. 主题 A 会将if条件评估为false
  4. 主题 B 评估if条件为false
  5. 从那时起,两个线程将继续执行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上