ConcurrentHashMap:使用"putIfAbsent"避免额外的对象创建?

Gen*_*sky 38 java synchronization thread-safety concurrenthashmap

我在多线程环境中聚合键的多个值.钥匙事先不知道.我以为我会这样做:

class Aggregator {
    protected ConcurrentHashMap<String, List<String>> entries =
                            new ConcurrentHashMap<String, List<String>>();
    public Aggregator() {}

    public void record(String key, String value) {
        List<String> newList =
                    Collections.synchronizedList(new ArrayList<String>());
        List<String> existingList = entries.putIfAbsent(key, newList);
        List<String> values = existingList == null ? newList : existingList;
        values.add(value);
    }
}
Run Code Online (Sandbox Code Playgroud)

我看到的问题是,每次运行此方法时,我都需要创建一个新的实例,ArrayList然后将其丢弃(在大多数情况下).这似乎是无理滥用垃圾收集器.是否有一种更好的,线程安全的方法来初始化这种结构而不必使用synchronizerecord方法?我对使该putIfAbsent方法不返回新创建的元素的决定感到有些惊讶,并且缺少一种延迟实例化的方法,除非它被调用(可以这么说).

Boh*_*ian 41

Java 8引入了一个API来满足这个确切的问题,制作了一个单行解决方案:

public void record(String key, String value) {
    entries.computeIfAbsent(key, k -> Collections.synchronizedList(new ArrayList<String>())).add(value);
}
Run Code Online (Sandbox Code Playgroud)

对于Java 7:

public void record(String key, String value) {
    List<String> values = entries.get(key);
    if (values == null) {
        entries.putIfAbsent(key, Collections.synchronizedList(new ArrayList<String>()));
        // At this point, there will definitely be a list for the key.
        // We don't know or care which thread's new object is in there, so:
        values = entries.get(key);
    }
    values.add(value);
}
Run Code Online (Sandbox Code Playgroud)

这是填充a时的标准代码模式ConcurrentHashMap.

特殊方法putIfAbsent(K, V))将把你的值对象放入,或者如果另一个线程在你之前,那么它将忽略你的值对象.无论哪种方式,在调用之后putIfAbsent(K, V)),get(key)保证在线程之间保持一致,因此上面的代码是线程安全的.

唯一浪费的开销是,如果某个其他线程同时为同一个键添加一个新条目:您可能最终丢弃新创建的值,但只有在没有条目并且有一个种族的情况下才会发生这种情况线程丢失,这通常是罕见的.

  • 所以`putIfAbsent`调用用null替换`values`变量; 此代码需要测试以查看`putIfAbsent`是否返回null. (4认同)
  • 我知道这是一个陈旧的答案,但@Erlend是正确的.另一个线程可能会删除对`putIfAbsent`和`get`的调用之间的条目.Gene的答案是正确的实现,它保证你不会在调用`values.add()`时抛出NPE. (3认同)
  • 如果第二个线程在get之前删除了条目怎么办? (2认同)

Pet*_*ter 15

从Java-8开始,您可以使用以下模式创建多地图:

public void record(String key, String value) { entries.computeIfAbsent(key, k -> Collections.synchronizedList(new ArrayList<String>())) .add(value); }

ConcurrentHashMap文档(不是常规协定)指定只为每个键创建一次ArrayList,而在为新键创建ArrayList时,延迟更新的初始成本很小:

http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ConcurrentHashMap.html#computeIfAbsent-K-java.util.function.Function-


Gen*_*sky 11

最后,我对@ Bohemian的回答稍作修改.他提出的解决方案valuesputIfAbsent调用覆盖变量,这就产生了我之前遇到的同样问题.似乎工作的代码如下所示:

    public void record(String key, String value) {
        List<String> values = entries.get(key);
        if (values == null) {
            values = Collections.synchronizedList(new ArrayList<String>());
            List<String> values2 = entries.putIfAbsent(key, values);
            if (values2 != null)
                values = values2;
        }
        values.add(value);
    }
Run Code Online (Sandbox Code Playgroud)

它并不像我想的那么优雅,但它比ArrayList每次通话时创建一个新实例的原始效果更好.

  • 从Java-8开始,您可以将其替换为:entries.computeIfAbsent(key,k - > Collections.synchronizedList(new ArrayList <String>())).add(value) (2认同)