2010-07-26 16 views
28

Me gustaría recopilar algunas métricas de varios lugares en una aplicación web. Para simplificar, todos estos serán contadores y, por lo tanto, la única operación modificadora es incrementarlos en 1.Contadores de incremento automático almacenados en ConcurrentHashMap

Los incrementos serán simultáneos y frecuentes. Las lecturas (volcar las estadísticas) es una operación rara.

Estaba pensando en utilizar un ConcurrentHashMap. El problema es cómo incrementar los contadores correctamente. Como el mapa no tiene una operación de "incremento", primero necesito leer el valor actual, incrementarlo en lugar de 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 del ConcurrentHashMap)? ¿Tengo que mirar Guava?

Gracias por cualquier apuntador.


P.S.
Hay una pregunta relacionada sobre SO (Most efficient way to increment a Map value in Java), pero centrado en el rendimiento y no multi-threading

ACTUALIZACIÓN
Para los que llegan aquí a través de búsquedas sobre el mismo tema: además de las respuestas a continuación, hay una utilidad presentation que incidentalmente cubre el mismo tema. Ver diapositivas 24-33.

Respuesta

7

Estás muy cerca. ¿Por qué no intentas algo como un ConcurrentHashMap<Key, AtomicLong>? Si sus Key s (métricas) no cambian, puede usar simplemente un estándar HashMap (son seguros para la lectura si se leen solo, pero sería conveniente que lo haga explícito con ImmutableMap de Google Collections o Collections.unmodifiableMap, etc.).

De esta manera, puede usar map.get(myKey).incrementAndGet() para mostrar las estadísticas.

+1

Pero no se olvide de almacenar el 'HashMap' en una 'final' miembro. Y mejor envuelva el mapa en un contenedor 'no modificable'. Mejor aún, puedes usar 'ImmutableMap' de Guava (superconjunto de la colección de google) y debería ser realmente muy rápido. –

+0

@Zwei: buen punto, editó la respuesta para incluir ese consejo :) –

+0

La lista de métricas se construye a medida que suministran datos (es decir, las claves del mapa se agregarán a medida que el sistema se ejecuta y se golpean varios puntos de recolección; priori sería propenso a errores). Me olvidé del incremento de AtomicLongAndGet(), es justo lo que necesito. Si no existiera, estaba pensando que otro enfoque habría sido que los recopiladores de métricas no aumentaran los contadores, sino simplemente agregar la solicitud para hacerlo en una cola mantenida por el singleton. Por lo tanto, las personas que llaman solo agregan() a una lista, que periódicamente se lee y procesa. No es tan simple, sin embargo. – wishihadabettername

4

Aparte de ir con AtomicLong, puede hacer lo habitual lo cas de circuito:

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 una do - bucle while para las edades.)

Para valores pequeños de la 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.

+0

Creo que es correcto. Disculpas a cualquiera que mire el código roto anterior. (Podrías reorganizarlo para que comience con un 'get', un comparar con null y luego probar 'putIfAbsent', continuando con un ciclo' while' normal) –

14

El nuevo AtomicLongMap de Guava (en la versión 11) podría cubrir esta necesidad.

+0

¡Esta es la respuesta perfecta! Compare la probabilidad de error de concurrencia entre Guava y las muestras de código personalizadas aquí enumeradas. – snowindy

0

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 llaves y 10 hilos por cada llave. Nada se perdió Aún no he probado varios hilos de descarga, pero espero que funcione.

Massive singleusermode flush me está torturando ... Quiero eliminar RWLock y descomponer el enjuague en trozos pequeños. 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(); 
    } 

} 
+0

Por cada vez que se llame a getCount, impedirá las escrituras. Esto garantizará la coherencia del valor. Pero tendrá un impacto en el rendimiento. – Sudhakar

20

En Java 8:

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

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

en forma abreviada con el enlace en el método: 'LongAdder :: incremento' – pacman

+0

@pacman ¿Puedes explicarlo? – ZhekaKozlov

+0

Quise decir el método java 8 en el enlace: 'AtomicInteger atomicInt = new AtomicInteger (0); atomicInt :: incrementAndGet' – pacman

-2

El siguiente código me funcionó sin sincronización para frecuencias palabras de contar

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)); 

      } 
     } 
+1

* El siguiente código funcionó para yo sin sincronización * es un indicador completamente sin sentido de la seguridad del hilo. – shmosel

+0

Downvoted. Este código puede fallar si se producen 3 intentos de agregar una clave en sucesión rápida. – Persixty

+0

¿Falló dónde? ¿en otra cosa o si rama? – user1264304

Cuestiones relacionadas