在使用ConcurrentMap的putIfAbsent之前,你应该检查map是否包含key

我一直在使用Java的ConcurrentMap来处理可以从多个线程使用的地图。 putIfAbsent是一个很好的方法,比使用标准的映射操作更容易读写。 我有一些看起来像这样的代码:

ConcurrentMap<String, Set<X>> map = new ConcurrentHashMap<String, Set<X>>(); // ... map.putIfAbsent(name, new HashSet<X>()); map.get(name).add(Y); 

可读性明智,但它确实需要每次创build一个新的HashSet,即使它已经在地图中。 我可以写这个:

 if (!map.containsKey(name)) { map.putIfAbsent(name, new HashSet<X>()); } map.get(name).add(Y); 

有了这个改变,它会失去一些可读性,但不需要每次都创buildHashSet。 在这种情况下哪个更好? 我倾向于第一个方面,因为它更可读。 第二个performance会更好,可能会更正确。 也许有比这两个更好的方法来做到这一点。

以这种方式使用putIfAbsent的最佳做法是什么?

并发性很难。 如果您打算使用并发地图而不是直接locking,那么您最好还是去做。 事实上,不要做太多的查找。

 Set<X> set = map.get(name); if (set == null) { final Set<X> value = new HashSet<X>(); set = map.putIfAbsent(name, value); if (set == null) { set = value; } } 

(平常的stackoverflow免责声明:closures我的头顶部。未testing。未编译。等等)

更新: 1.8已将computeIfAbsent默认方法添加到ConcurrentMap (和Map有趣,因为该实现对于ConcurrentMap是错误的)。 (1.7添加了“钻石运算符” <> 。)

 Set<X> set = map.computeIfAbsent(name, n -> new HashSet<>()); 

(请注意,您对ConcurrentMap包含的HashSet的任何操作的线程安全性负责。)

汤姆的答案是正确的,只要使用API​​的ConcurrentMap。 避免使用putIfAbsent的另一种方法是使用GoogleCollections / Guava MapMaker中的计算映射,它使用提供的函数自动填充值,并处理所有线程安全性。 它实际上只为每个键创build一个值,如果创build函数是昂贵的,其他线程要求获得相同的键会阻塞,直到值变为可用。

从Guava 11 编辑 ,MapMaker已被弃用,并被replace为Cache / LocalCache / CacheBuilder的东西。 这在使用上稍微复杂一点,但基本上是同构的。

您可以使用Eclipse Collections (以前的GS Collections )中的MutableMap.getIfAbsentPut(K, Function0<? extends V>) )。

调用get() ,做一个空的检查,然后调用putIfAbsent()是我们只计算一次key的hashCode,并在hashtable中find正确的位置。 在像org.eclipse.collections.impl.map.mutable.ConcurrentHashMap这样的ConcurrentMaps中, getIfAbsentPut()的实现也是线程安全的和primefaces的。

 import org.eclipse.collections.impl.map.mutable.ConcurrentHashMap; ... ConcurrentHashMap<String, MyObject> map = new ConcurrentHashMap<>(); map.getIfAbsentPut("key", () -> someExpensiveComputation()); 

org.eclipse.collections.impl.map.mutable.ConcurrentHashMap的实现是真正的非阻塞的。 尽pipe不必要地调用工厂函数,但在争用过程中仍然有可能被多次调用。

这个事实将它与Java 8的ConcurrentHashMap.computeIfAbsent(K, Function<? super K,? extends V>)区别开来。 该方法的Javadoc指出:

整个方法的调用是以primefaces方式执行的,所以每个键的function最多应用一次。 计算正在进行时,其他线程在此映射上的某些尝试更新操作可能会被阻止,所以计算应该简短而且…

注意:我是Eclipse集合的提交者。

通过保持每个线程的预初始化值,您可以改进接受的答案:

 Set<X> initial = new HashSet<X>(); ... Set<X> set = map.putIfAbsent(name, initial); if (set == null) { set = initial; initial = new HashSet<X>(); } set.add(Y); 

我最近使用AtomicInteger映射值而不是Set。

在5年多的时间里,我不敢相信没有人提及或发布了一个使用ThreadLocal解决这个问题的解决scheme, 在这个页面上的几个解决scheme不是线程安全的 ,只是马虎。

对于这个特定的问题,使用ThreadLocals不仅被认为是并发性的最佳实践 ,而且是为了线程争用期间最小化垃圾/对象的创build。 此外,它是令人难以置信的干净的代码。

例如:

 private final ThreadLocal<HashSet<X>> threadCache = new ThreadLocal<HashSet<X>>() { @Override protected HashSet<X> initialValue() { return new HashSet<X>(); } }; private final ConcurrentMap<String, Set<X>> map = new ConcurrentHashMap<String, Set<X>>(); 

而实际的逻辑…

 // minimize object creation during thread contention final Set<X> cached = threadCache.get(); Set<X> data = map.putIfAbsent("foo", cached); if (data == null) { // reset the cached value in the ThreadLocal listCache.set(new HashSet<X>()); data = cached; } // make sure that the access to the set is thread safe synchronized(data) { data.add(object); } 

我的通用近似值:

 public class ConcurrentHashMapWithInit<K, V> extends ConcurrentHashMap<K, V> { private static final long serialVersionUID = 42L; public V initIfAbsent(final K key) { V value = get(key); if (value == null) { value = initialValue(); final V x = putIfAbsent(key, value); value = (x != null) ? x : value; } return value; } protected V initialValue() { return null; } } 

并作为使用的例子:

 public static void main(final String[] args) throws Throwable { ConcurrentHashMapWithInit<String, HashSet<String>> map = new ConcurrentHashMapWithInit<String, HashSet<String>>() { private static final long serialVersionUID = 42L; @Override protected HashSet<String> initialValue() { return new HashSet<String>(); } }; map.initIfAbsent("s1").add("chao"); map.initIfAbsent("s2").add("bye"); System.out.println(map.toString()); }