【问题标题】:Should you check if the map containsKey before using ConcurrentMap's putIfAbsent您是否应该在使用 ConcurrentMap 的 putIfAbsent 之前检查地图是否包含密钥
【发布时间】:2011-04-14 17:03:17
【问题描述】:

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

【问题讨论】:

  • 在您的示例中,Value-HashSet 也需要是 ConcurrentHashSet,否则这仍然不是线程安全的。
  • Tom Hawtin 的解决方案正是您想要的
  • 正如 Markus 所指出的,值类型(在这种情况下为 Set)确实需要也是线程安全的,因为它可以被多个线程同时访问。
  • 即使你使用Tom Hawtin的回复,也是不完整的,我相信和你自己的建议是等价的。

标签: java performance concurrency concurrenthashmap


【解决方案1】:

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

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

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

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

【讨论】:

  • +1 表示“并发很难”并使用 putIfAbsent 的返回值
  • @Markus - 也为您 +1 指出了一个显而易见但容易忽略的事实,即重用返回值是一种好习惯。
  • 好答案。让我想起了双重检查锁定:en.wikipedia.org/wiki/Double-checked_locking
  • 如果多个线程同时未通过if (set == null) 检查,它是否仍会实例化多个HashSet 实例?
  • @zerkms 这可能会发生,第二个if (set == null) 处理第一个线程通过获胜。这种情况不太可能发生,额外的分配时间不太可能很重要。您可以插入一个临时值以提供单线程独占访问权限,但这可能会使一般性能和可靠性变差。
【解决方案2】:

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

Edit 从 Guava 11 开始,MapMaker 已被弃用,取而代之的是 Cache/LocalCache/CacheBuilder 的东西。这在使用上稍微复杂一些,但基本上是同构的。

【讨论】:

  • 我刚试过这个,这是一个很好的解决方案。您可以获得 ConcurrentMap 的所有好处,而不必担心 putIfAbsent 惯用语,这些惯用语很容易搞砸。
【解决方案3】:

您可以从Eclipse Collections(以前的GS Collections)使用MutableMap.getIfAbsentPut(K, Function0&lt;? extends V&gt;)

与调用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&lt;? super K,? extends V&gt;) 不同。此方法的 Javadoc 指出:

整个方法调用都是原子执行的,所以函数 每个键最多应用一次。一些尝试的更新操作 计算进行时,其他线程的此映射可能会被阻塞 进步,所以计算应该简短而简单......

注意:我是 Eclipse Collections 的提交者。

【讨论】:

  • 我非常喜欢这个。几年前我问过这个问题,但对于 Java 8,这是一个非常好的解决方案。
【解决方案4】:

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

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。

【讨论】:

  • 如已接受答案的更新中所述,Java 1.8 添加了 computeIfAbsent,它实现了相同的结果并且更简单。
  • 这段代码需要使用的上下文会很棘手。如果不是直截了当,它会导致“有趣”地在路上找到错误。我也不相信这是性能上的胜利。 (另外你需要锁定对HashSet的访问。)
【解决方案5】:

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);
}

【讨论】:

  • 这是“线程共享”。 ThreadLocal 是为了防止在“putIfAbsent()”调用期间创建不必要的对象(Map,因此创建 Set 并不便宜)。该集合已正确且安全地发布到所有线程。
  • 当与putIfAbsent()结合时,只有一个会赢。因此data 在所有线程中总是相同的。
  • 我认为这个性能不会很好。每次您只需要ConcurrentMap.get 时,您都必须通过ThreadLocal。 (而且您对HashSet 的访问不是线程安全的。)
  • 它在并发映射上命中锁,因此根据 JSL 第 17.4 章和锁的一般行为,(编辑)它是可见的,但不是线程安全的。感谢您指出这一点,我已经更新了答案。您对性能是正确的……但是,如果想要性能,则不会使用并发映射或(默认)本地线程,而是使用非常不同的数据结构;这远远超出了这个问题的范围。
  • 我还应该指出,对 ThreadLocal 的性能影响远低于创建和实例化新对象(特别是 Set...)时的性能影响...
【解决方案6】:

我的通用近似值:

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-10
    • 1970-01-01
    • 2011-02-16
    • 2013-08-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多