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


71

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


3
在您的示例中,Value-HashSet也需要也是ConcurrentHashSet,否则这仍然不是线程安全的。
Markus Kull

1
汤姆Hawtin的解决方案正是您所渴望的
约翰的Vint

1
正如Markus指出的那样,值类型(在这种情况下为Set)的确也必须是线程安全的,因为它可以被多个线程同时访问。
sjlee 2010年

即使您使用Tom Hawtin的回复(此回复不完整),我相信也等同于您自己的建议。
petersaints,

Answers:


106

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

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

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


23
+1表示“并发性很强”,并使用putIfAbsent的返回值
Markus Kull 2010年

1
@Markus-还向您+1指出了显而易见的但很容易忽略的事实,即重用returnvalue是一个好习惯。
2011年

1
好答案。让我想起了双重检查锁定:en.wikipedia.org/wiki/Double-checked_locking
Mansoor Siddiqui 2012年

1
HashSet如果几个线程同时未能通过if (set == null)检查,它是否还会实例化几个实例?
zerkms 2013年

2
@zerkms可能会发生,第二个人if (set == null)则通过赢得胜利来处理第一件事。这种情况不太可能发生,并且额外的分配时间不太可能会很大。您可以插入一个临时值以给予单线程独占访问权限,但这可能会使总体性能和可靠性变差。
Tom Hawtin-大头钉

16

就ConcurrentMap的API使用而言,Tom的答案是正确的。避免使用putIfAbsent的另一种方法是使用GoogleCollections / Guava MapMaker中的计算图,后者使用提供的函数自动填充值并为您处理所有线程安全性。实际上,每个键仅创建一个值,如果create函数昂贵,则其他要求获取同一键的线程将阻塞,直到该值可用为止。

从Guava 11中进行编辑,MapMaker已弃用,并由Cache / LocalCache / CacheBuilder替换。它的用法有些复杂,但基本上是同构的。


我刚刚尝试过,这是一个很好的解决方案。您可以获得ConcurrentMap的所有好处,而不必担心putIfAbsent习惯用法,这些习惯用法很容易弄乱。
马特·弗里德曼

5

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

与先调用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
我很喜欢这个 几年前我问了这个问题,但是对于Java 8,这是一个非常好的解决方案。
克里斯·戴尔

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。


如对已接受答案的更新中所述,Java 1.8添加了computeIfAbsent,可实现相同的结果,并且更加简单。
karmakaze

需要使用此代码的上下文非常棘手。如果不是直接的话,这将导致“有趣的”发现错误。我也不相信这也是一场表演冠军。(此外,您还需要锁定访问权限HashSet。)
Tom Hawtin-tackline

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

这是“线程共享的”。ThreadLocal可以防止在“ putIfAbsent()”调用期间不必要的对象创建(映射,因此Set创建并不便宜)。该集已正确且安全地发布到所有线程。
弥敦道

当与组合使用时putIfAbsent(),只有一个获胜。因此data,所有线程之间始终是相同的。
内森

我认为这样做的效果不会很好。你必须去通过ThreadLocal每一次你只是需要一个时间之外ConcurrentMap.get。(并且您访问的HashSet 不是线程安全的。)
Tom Hawtin-stickline

它在并发映射上命中了锁,因此根据JSL第17.4章和锁的一般行为,(编辑)它是可见的,但不是线程安全的。感谢您指出,我已经更新了答案。您对性能的看法是正确的……但是,如果要提高性能,则不会使用并发映射或本地(默认)线程,而要使用完全不同的数据结构;这是方法,超出了此问题的范围。
内森

我还应该指出,对ThreadLocal的性能影响要比创建和实例化新对象(特别是Set)的性能影响要小得多
Nathan

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());
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.