【问题标题】:Atomically incrementing counters stored in ConcurrentHashMap以原子方式递增存储在 ConcurrentHashMap 中的计数器
【发布时间】:2011-03-21 08:49:53
【问题描述】:

我想从网络应用程序的不同位置收集一些指标。为简单起见,所有这些都是计数器,因此唯一的修饰符操作是将它们递增 1。

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

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

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

感谢您的任何指点。


附言
有一个关于 SO (Most efficient way to increment a Map value in Java) 的相关问题,但关注的是性能而不是多线程

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

【问题讨论】:

    标签: java multithreading concurrency guava concurrenthashmap


    【解决方案1】:

    在 Java 8 中:

    ConcurrentHashMap<String, LongAdder> map = new ConcurrentHashMap<>();
    
    map.computeIfAbsent("key", k -> new LongAdder()).increment();
    

    【讨论】:

    • 带有方法链接的简短形式:LongAdder::increment
    • 我的意思是链接上的 java 8 方法:AtomicInteger atomicInt = new AtomicInteger(0); atomicInt::incrementAndGet
    • 您可以在上面的代码中使用“更多功能”的方法
    • @pacman 你能提供准确的代码吗?我不认为我的答案可以更实用。
    • Map&lt;String, LongAdder&gt; map = new ConcurrentHashMap&lt;&gt;(); Consumer&lt;LongAdder&gt; function = LongAdder::increment; function.accept(map.get("key"));
    【解决方案2】:

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

    【讨论】:

    • 这是一个完美的答案!比较 Guava 与此处列出的自定义代码示例之间的并发错误概率。
    【解决方案3】:

    你已经很接近了。你为什么不试试ConcurrentHashMap&lt;Key, AtomicLong&gt; 之类的东西? 如果您的Keys(指标)不变,您甚至可以只使用标准的HashMap(如果它们是只读的,它们是线程安全的,但建议您使用来自Google Collections 的ImmutableMap 或@ 987654325@等)。

    这样,您可以使用map.get(myKey).incrementAndGet() 进行统计。

    【讨论】:

    • 别忘了将HashMap 存储在final 成员中。并且更好地将地图包装在 unmodifiable 包装器中。更好的是,您可以使用来自 Guava(谷歌集合的超集)的 ImmutableMap,它应该非常快。
    • @Zwei:好点,编辑了答案以包含该建议:)
    • 指标列表是在它们提供数据时构建的(即,地图的键将在系统运行和各种收集点被命中时添加;先验地构建列表很容易出错)。我忘记了 AtomicLong 的 incrementAndGet(),这正是我所需要的。如果它不存在,我在想另一种方法是度量收集器不增加计数器,而只是将请求添加到由单例维护的队列中。因此,调用者只需将 add() 添加到一个列表中,该列表会定期被读取和处理。不过没那么简单。
    【解决方案4】:

    除了使用AtomicLong,您还可以执行通常的 cas-loop 操作:

    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 可能会被“缓存”。对于较长的值,可能需要分配。但是分配实际上非常快(并且您可以进一步缓存) - 取决于您的期望,在最坏的情况下。

    【讨论】:

    • 我认为是对的。向任何查看以前损坏的代码的人道歉。 (您可以重新排列它,使其以 get 开头,与 null 进行比较,然后再尝试 putIfAbsent,继续正常的 while 循环。)
    【解决方案5】:

    有必要这样做。 我正在使用 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();
        }
    
    }
    

    【讨论】:

    • 每次调用 getCount 都会阻止写入。这将保证值的一致性。但会影响性能。
    【解决方案6】:

    我做了一个基准来比较LongAdderAtomicLong 的性能。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-12-24
      • 2015-01-04
      • 1970-01-01
      • 2014-09-05
      • 2010-12-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多