java - repetidas - Contadores de incremento atómico almacenados en ConcurrentHashMap
java map con claves repetidas (6)
Me gustaría recopilar algunas métricas de varios lugares en una aplicación web. Para mantenerlo simple, todos estos serán contadores y, por lo tanto, la única operación modificadora es incrementarlos en 1.
Los incrementos serán concurrentes y a menudo. Las lecturas (volcar las estadísticas) es una operación rara.
Estaba pensando en usar un mapa de fichas concurrente . El problema es cómo incrementar los contadores correctamente. Dado que el mapa no tiene una operación de "incremento", primero necesito leer el valor actual, incrementarlo para poner el nuevo valor en el mapa. Sin más código, esta no es una operación atómica.
¿Es posible lograr esto sin sincronización (lo que anularía el propósito de ConcurrentHashMap )? ¿Debo mirar a Guava ?
Gracias por cualquier punteros.
PD
Hay una pregunta relacionada sobre SO (la forma más eficiente de incrementar un valor de Map en Java ) pero enfocada en el rendimiento y no en multiproceso
ACTUALIZAR
Para aquellos que llegan aquí a través de búsquedas sobre el mismo tema: además de las respuestas a continuación, hay una presentation útil que incidentalmente cubre el mismo tema. Ver diapositivas 24-33.
Además de ir con AtomicLong
, puedes hacer lo de cas-loop habitual:
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.
}
(No he escrito un bucle do
- while por años).
Para valores pequeños, el Long
probablemente se "almacenará en caché". Para valores más largos, puede requerir asignación. Pero las asignaciones son en realidad extremadamente rápidas (y puede almacenar en caché aún más), depende de lo que espere, en el peor de los casos.
El nuevo AtomicLongMap Guava (en la versión 11) podría abordar esta necesidad.
El siguiente código funcionó para mí sin sincronización para contar las frecuencias de las palabras
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));
}
}
En Java 8:
ConcurrentHashMap<String, LongAdder> map = new ConcurrentHashMap<>();
map.computeIfAbsent("key", k -> new LongAdder()).increment();
Estas bastante cerca ¿Por qué no intentas algo como un ConcurrentHashMap<Key, AtomicLong>
? Si sus Key
s (métricas) no cambian, puede usar simplemente un HashMap
estándar (son seguras para hilos si son de solo lectura, pero sería recomendable que lo explicite con ImmutableMap
de Google Collections o Collections.unmodifiableMap
, etc.) .
De esta forma, puede usar map.get(myKey).incrementAndGet()
para encontrar estadísticas.
Tengo una necesidad de hacer lo mismo. Estoy usando ConcurrentHashMap + AtomicInteger. Además, se introdujo ReentrantRW Lock para descarga atómica (comportamiento muy similar).
Probado con 10 claves y 10 hilos por cada clave. Nada se perdió. Todavía no he probado varios hilos de lavado, pero espero que funcione.
El flujo masivo de modo único me está torturando ... Quiero eliminar RWLock y romper el lavado en pequeños pedazos. Mañana.
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();
}
}