74

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

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 的最佳做法是什么?

4

6 回答 6

108

并发很难。如果您要为并发映射而不是直接锁定而烦恼,那么您不妨试试。确实,不要进行不必要的查找。

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默认方法ConcurrentMapMap这很有趣,因为该实现对于 来说是错误的ConcurrentMap)。(并且 1.7 添加了“钻石运算符” <>。)

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

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

于 2010-09-20T14:11:15.817 回答
16

就 ConcurrentMap 的 API 使用而言,汤姆的回答是正确的。避免使用 putIfAbsent 的另一种方法是使用来自 GoogleCollections/Guava MapMaker 的计算映射,它使用提供的函数自动填充值并为您处理所有线程安全。它实际上只为每个键创建一个值,如果创建函数很昂贵,其他请求获取相同键的线程将阻塞,直到该值可用。

从 Guava 11 开始编辑,MapMaker 已被弃用,并被 Cache/LocalCache/CacheBuilder 的东西所取代。这在使用上有点复杂,但基本上是同构的。

于 2010-09-21T02:23:34.847 回答
5

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

get()与调用、执行空检查然后调用相比的优势putIfAbsent()在于,我们只需计算一次键的 hashCode,并在哈希表中找到正确的位置一次。在 ConcurrentMaps likeorg.eclipse.collections.impl.map.mutable.ConcurrentHashMap中, 的实现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 的提交者。

于 2014-07-10T17:31:08.157 回答
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 一起使用。

于 2013-08-19T20:05:07.810 回答
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);
}
于 2016-01-14T00:50:53.277 回答
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());
}
于 2014-05-26T20:49:11.137 回答