Ответ 1
В Java 8:
ConcurrentHashMap<String, LongAdder> map = new ConcurrentHashMap<>();
map.computeIfAbsent("key", k -> new LongAdder()).increment();
Я хотел бы собрать некоторые метрики из разных мест в веб-приложении. Для простоты все они будут счетчиками, и поэтому единственной операцией модификатора является увеличение их на 1.
Приращения будут параллельными и частыми. Чтение (сброс статистики) - редкая операция.
Я думал использовать ConcurrentHashMap. Вопрос в том, как правильно увеличить счетчики. Поскольку на карте нет операции "увеличения", мне нужно сначала прочитать текущее значение, увеличить его, а затем поместить новое значение на карту. Без дополнительного кода это не атомарная операция.
Можно ли достичь этого без синхронизации (что противоречит цели ConcurrentHashMap)? Нужно ли смотреть на гуаву?
Спасибо за любые указатели.
PS
Есть связанный вопрос о SO (наиболее эффективный способ увеличить значение Map в Java), но он сосредоточен на производительности, а не на многопоточности.
ОБНОВИТЬ
Для тех, кто прибывает сюда через поиск по той же теме: помимо ответов ниже, есть полезная презентация, которая случайно затрагивает ту же тему. Смотрите слайды 24-33.
В Java 8:
ConcurrentHashMap<String, LongAdder> map = new ConcurrentHashMap<>();
map.computeIfAbsent("key", k -> new LongAdder()).increment();
Guava new AtomicLongMap (в версии 11) может решить эту проблему.
Ты довольно близко. Почему бы вам не попробовать что-то вроде ConcurrentHashMap<Key, AtomicLong>
?
Если ваши Key
(метрики) неизменны, вы даже можете просто использовать стандартный HashMap
(они являются потокобезопасными, если только с чтением, но вам было бы полезно сделать это явным с помощью ImmutableMap
из Коллекций Google или Collections.unmodifiableMap
и т.д.).
Таким образом, вы можете использовать map.get(myKey).incrementAndGet()
для отображения статистики.
Помимо перехода с 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
, вероятно, будет "кэшироваться". Для более длинных значений может потребоваться распределение. Но распределения на самом деле чрезвычайно быстрые (и вы можете кэшировать дальше) - зависит от того, что вы ожидаете, в худшем случае.
Появилась необходимость сделать то же самое. Я использую 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();
}
}
Я сделал тест для сравнения производительности LongAdder
и AtomicLong
.
LongAdder
лучшую производительность в моем тесте: для 500 итераций с использованием карты размером 100 (10 одновременных потоков) среднее время для LongAdder составляло 1270 мс, а для AtomicLong - 1315 мс.
Следующий код работал у меня без синхронизации для подсчета частот слов
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));
}
}