以原子方式递增存储在ConcurrentHashMap中的计数器

时间:2010-07-26 23:48:10

标签: java multithreading concurrency guava concurrenthashmap

我想从网络应用中的各个地方收集一些指标。为了简单起见,所有这些都是计数器,因此唯一的修饰符操作是将它们递增1.

增量将是并发且经常的。读取(转储统计数据)是一种罕见的操作。

我正在考虑使用 ConcurrentHashMap 。问题是如何正确递增计数器。由于地图没有“增量”操作,我需要首先读取当前值,增加它而不是将新值放在地图中。没有更多代码,这不是原子操作。

是否有可能在没有同步的情况下实现这一点(这会破坏 ConcurrentHashMap 的目的)?我需要查看Guava吗?

感谢您的任何指示。


P.S。
关于SO(Most efficient way to increment a Map value in Java)有一个相关问题,但侧重于性能而不是多线程

更新
对于那些通过搜索相同主题到达此处的人:除了下面的答案之外,还有一个有用的presentation,它偶然涵盖了相同的主题。见幻灯片24-33。

7 个答案:

答案 0 :(得分:33)

在Java 8中:

ConcurrentHashMap<String, LongAdder> map = new ConcurrentHashMap<>();

map.computeIfAbsent("key", k -> new LongAdder()).increment();

答案 1 :(得分:18)

Guava的新AtomicLongMap(在第11版中)可能会满足这种需求。

答案 2 :(得分:8)

你非常接近。你为什么不试试像ConcurrentHashMap<Key, AtomicLong>这样的东西? 如果您的Key s(指标)不变,您甚至可以使用标准HashMap(如果只读,它们是线程安全的,但建议您使用{{1}进行明确说明来自Google Collections或ImmutableMap等)。

这样,您就可以使用Collections.unmodifiableMap来统计数据。

答案 3 :(得分:4)

除了使用AtomicLong之外,您可以执行通常的cas循环操作:

private final ConcurrentMap<Key,Long> counts =
    new ConcurrentHashMap<Key,Long>();

public void increment(Key key) {
    if (counts.putIfAbsent(key, 1)) == null) {
        return;
    }

    Long old;
    do {
       old = counts.get(key);
    } while (!counts.replace(key, old, old+1)); // Assumes no removal.
}

(我多年没有写过do - while循环。)

对于小值,Long可能会被“缓存”。对于更长的值,可能需要分配。但是分配实际上非常快(并且你可以进一步缓存) - 取决于你期望的,在最坏的情况下。

答案 4 :(得分:0)

有必要这样做。 我正在使用ConcurrentHashMap + AtomicInteger。 此外,引入了ReentrantRW Lock用于原子冲洗(非常相似的行为)。

每个密钥使用10个密钥和10个线程进行测试。什么都没有丢失。 我还没有尝试过几个冲洗线程,但希望它会起作用。

庞大的单用户模式冲洗让我折磨...... 我想删除RWLock并将其冲洗成小块。明天。

private ConcurrentHashMap<String,AtomicInteger> counters = new ConcurrentHashMap<String, AtomicInteger>();
private ReadWriteLock rwLock = new ReentrantReadWriteLock();

public void count(String invoker) {

    rwLock.readLock().lock();

    try{
        AtomicInteger currentValue = counters.get(invoker);
        // if entry is absent - initialize it. If other thread has added value before - we will yield and not replace existing value
        if(currentValue == null){
            // value we want to init with
            AtomicInteger newValue = new AtomicInteger(0);
            // try to put and get old
            AtomicInteger oldValue = counters.putIfAbsent(invoker, newValue);
            // if old value not null - our insertion failed, lets use old value as it's in the map
            // if old value is null - our value was inserted - lets use it
            currentValue = oldValue != null ? oldValue : newValue;
        }

        // counter +1
        currentValue.incrementAndGet();
    }finally {
        rwLock.readLock().unlock();
    }

}

/**
 * @return Map with counting results
 */
public Map<String, Integer> getCount() {
    // stop all updates (readlocks)
    rwLock.writeLock().lock();
    try{
        HashMap<String, Integer> resultMap = new HashMap<String, Integer>();
        // read all Integers to a new map
        for(Map.Entry<String,AtomicInteger> entry: counters.entrySet()){
            resultMap.put(entry.getKey(), entry.getValue().intValue());
        }
        // reset ConcurrentMap
        counters.clear();
        return resultMap;

    }finally {
        rwLock.writeLock().unlock();
    }

}

答案 5 :(得分:0)

我做了一个基准测试,比较了LongAdderAtomicLong的性能。

LongAdder在我的基准测试中具有更好的性能:对于使用大小为100(10个并发线程)的映射进行500次迭代,LongAdder的平均时间为1270ms,而AtomicLong的平均时间为1315ms。

答案 6 :(得分:-3)

以下代码对我来说没有同步来计算单词&#39;频率

constructors