Подтвердить что ты не робот

Атомно увеличивающие счетчики, хранящиеся в ConcurrentHashMap

Я хотел бы собрать некоторые метрики из разных мест в веб-приложении. Для простоты все они будут счетчиками, и поэтому единственной операцией модификатора является увеличение их на 1.

Приращения будут параллельными и частыми. Чтение (сброс статистики) - редкая операция.

Я думал использовать ConcurrentHashMap. Вопрос в том, как правильно увеличить счетчики. Поскольку на карте нет операции "увеличения", мне нужно сначала прочитать текущее значение, увеличить его, а затем поместить новое значение на карту. Без дополнительного кода это не атомарная операция.

Можно ли достичь этого без синхронизации (что противоречит цели ConcurrentHashMap)? Нужно ли смотреть на гуаву?

Спасибо за любые указатели.


PS
Есть связанный вопрос о SO (наиболее эффективный способ увеличить значение Map в Java), но он сосредоточен на производительности, а не на многопоточности.

ОБНОВИТЬ
Для тех, кто прибывает сюда через поиск по той же теме: помимо ответов ниже, есть полезная презентация, которая случайно затрагивает ту же тему. Смотрите слайды 24-33.

4b9b3361

Ответ 1

В Java 8:

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

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

Ответ 2

Guava new AtomicLongMap (в версии 11) может решить эту проблему.

Ответ 3

Ты довольно близко. Почему бы вам не попробовать что-то вроде ConcurrentHashMap<Key, AtomicLong>? Если ваши Key (метрики) неизменны, вы даже можете просто использовать стандартный HashMap (они являются потокобезопасными, если только с чтением, но вам было бы полезно сделать это явным с помощью ImmutableMap из Коллекций Google или Collections.unmodifiableMap и т.д.).

Таким образом, вы можете использовать map.get(myKey).incrementAndGet() для отображения статистики.

Ответ 4

Помимо перехода с AtomicLong, вы можете выполнить обычную игру в кассе:

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, вероятно, будет "кэшироваться". Для более длинных значений может потребоваться распределение. Но распределения на самом деле чрезвычайно быстрые (и вы можете кэшировать дальше) - зависит от того, что вы ожидаете, в худшем случае.

Ответ 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 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();
    }

}

Ответ 6

Я сделал тест для сравнения производительности LongAdder и AtomicLong.

LongAdder лучшую производительность в моем тесте: для 500 итераций с использованием карты размером 100 (10 одновременных потоков) среднее время для LongAdder составляло 1270 мс, а для AtomicLong - 1315 мс.

Ответ 7

Следующий код работал у меня без синхронизации для подсчета частот слов

protected void updateFreqCountForText(Map<String, Integer> wordFreq, String line) {
            ConcurrentMap<String, Integer> wordFreqConc = (ConcurrentMap<String, Integer>) wordFreq;
            Pattern pattern = Pattern.compile("[a-zA-Z]+");
            Matcher matcher  = pattern.matcher(line);
            while (matcher.find()) {
                String word = matcher.group().toLowerCase();
                Integer oldCount;

                oldCount = wordFreqConc.get(word);
                if (oldCount == null) {
                    oldCount = wordFreqConc.putIfAbsent(word, 1);
                    if (oldCount == null)
                        continue;
                    else wordFreqConc.put(word, oldCount + 1);
                }
                else
                    do {
                        oldCount = wordFreqConc.get(word);
                    } while (!wordFreqConc.replace(word, oldCount, oldCount + 1));

            }
        }