我一直在使用Java的ConcurrentMap作为可以在多个线程中使用的地图。 putIfAbsent是一个很好的方法,比使用标准的map操作更容易读/写。我有一些看起来像这样的代码:
ConcurrentMap<String, Set<X>> map = new ConcurrentHashMap<String, Set<X>>();
// ...
map.putIfAbsent(name, new HashSet<X>());
map.get(name).add(Y);
可读性明智这很好,但它确实需要每次创建一个新的HashSet,即使它已经在地图中。我可以这样写:
if (!map.containsKey(name)) {
map.putIfAbsent(name, new HashSet<X>());
}
map.get(name).add(Y);
通过此更改,它会失去一点可读性,但不需要每次都创建HashSet。在这种情况下哪个更好?我倾向于支持第一个,因为它更具可读性。第二个会表现得更好,可能更正确。也许有更好的方法来做到这一点。
以这种方式使用putIfAbsent的最佳做法是什么?
答案 0 :(得分:105)
并发很难。如果您打算使用并发映射而不是直接锁定,那么您也可以选择它。实际上,不要进行超过必要的查找。
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免责声明:脱离我的头脑。未经测试。未编译。等等。)
更新 1.8已将computeIfAbsent
默认方法添加到ConcurrentMap
(和Map
,这有点令人感兴趣,因为{{1 }})。 (并且1.7添加了“钻石操作员”ConcurrentMap
。)
<>
(注意,您负责Set<X> set = map.computeIfAbsent(name, n -> new HashSet<>());
中包含的HashSet
的任何操作的线程安全性。)
答案 1 :(得分:16)
就ConcurrentMap的API使用情况而言,Tom的答案是正确的。避免使用putIfAbsent的替代方法是使用来自GoogleCollections / Guava MapMaker的计算映射,该映射使用提供的函数自动填充值并为您处理所有线程安全。它实际上只为每个键创建一个值,如果创建函数很昂贵,其他要求获取相同键的线程将阻塞,直到该值可用。
来自Guava 11的编辑,不推荐使用MapMaker并将其替换为Cache / LocalCache / CacheBuilder。这在使用上稍微复杂一些,但基本上是同构的。
答案 2 :(得分:5)
您可以使用MutableMap.getIfAbsentPut(K, Function0<? extends V>)
(以前为Eclipse Collections)中的GS Collections。
调用get()
,进行空检查,然后调用putIfAbsent()
的优点是我们只计算一次密钥的hashCode,并在哈希表中找到一次正确的位置。在像org.eclipse.collections.impl.map.mutable.ConcurrentHashMap
这样的ConcurrentMaps中,getIfAbsentPut()
的实现也是线程安全的和原子的。
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
的实施确实无阻塞。虽然我们尽一切努力不会不必要地调用工厂功能,但在争用期间仍有可能不止一次调用它。
这一事实使它与Java 8的ConcurrentHashMap.computeIfAbsent(K, Function<? super K,? extends V>)
区别开来。此方法的Javadoc声明:
整个方法调用是以原子方式执行的,所以函数 每个键最多应用一次。一些尝试更新操作 当计算进入时,其他线程的这个地图可能会被阻止 进展,所以计算应该简短......
注意:我是Eclipse Collections的提交者。
答案 3 :(得分:3)
通过为每个线程保留一个预先初始化的值,您可以改进接受的答案:
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。
答案 4 :(得分:2)
在5年多的时间里,我无法相信没有人提及或发布过使用 ThreadLocal 解决此问题的解决方案;此页面上的几个解决方案不是线程安全的,而且只是草率。
将ThreadLocals用于此特定问题不仅被视为并发的最佳实践,而且还用于在线程争用期间最小化的垃圾/对象创建。此外,这是非常干净的代码。
例如:
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);
}
答案 5 :(得分:0)
我的通用近似值:
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());
}