在使用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()); }