在使用ConcurrentMap的putIfAbsent之前,你应该检查地图是否包含Key

时间:2010-09-20 14:02:48

标签: java performance concurrency concurrenthashmap

我一直在使用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的最佳做法是什么?

6 个答案:

答案 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());
}