Seguridad de subprocesos con ConcurrentHashMap

Tengo la siguiente clase. Yo uso ConcurrentHashMap. Tengo muchos hilos escribiendo en los mapas y un temporizador que guarda los datos en el mapa cada 5 minutos. Logro lograr la seguridad de subprocesos usando putIfAbsent() cuando escribo entradas en el mapa. Sin embargo, cuando lo leo y luego elimino todas las entradas con el método clear(), no quiero que se escriba ningún otro subproceso en el mapa mientras estoy en el proceso de leer el contenido del mapa y luego eliminarlo. Obviamente, mi código no es seguro para subprocesos incluso con sincronizado (bloqueo) {}, porque el subproceso que posee el bloqueo en saveEntries(), no es necesariamente el mismo subproceso que escribe en mis mapas en el método log(). ¡A menos que bloquee todo el código en log() con el mismo objeto de bloqueo!

Me preguntaba si hay alguna otra forma de lograr la seguridad de subprocesos sin forzar la sincronización mediante un bloqueo externo. Cualquier ayuda es muy apreciada.

public class Logging {

private static Logging instance;    
private static final String vendor1 = "vendor1";
private static final String vendor2 = "vendor2";    
private static long delay = 5 * 60 * 1000;

private ConcurrentMap<String, Event> vendor1Calls = new ConcurrentHashMap<String, Event>();
private ConcurrentMap<String, Event> vendor2Calls = new ConcurrentHashMap<String, Event>();

private Timer timer;    
private final Object lock = new Object();

private Logging(){
    timer = new Timer();                
    timer.schedule(new TimerTask() {
        public void run() {
            try {
                saveEntries();
            } catch (Throwable t) {
                timer.cancel();
                timer.purge();
            }
        }       
    }, 0, delay);
}

public static synchronized Logging getInstance(){     
    if (instance == null){
        instance = new Logging();
    }
    return instance;
 }

public void log(){      
    ConcurrentMap<String, Event> map;
    String key = "";        

    if (vendor1.equalsIgnoreCase(engine)){
        map = vendor1Calls;
    }else if(vendor2.equalsIgnoreCase(engine)){  
        map = vendor2Calls;
    }else{
        return;
    }       


    key = service + "." + method;
// It would be the code if I use a regular HashMap instead of ConcurrentHashMap
    /*Event event = map.get(key);       

    // Map does not contain this service.method, create an Event for the first     time.
    if(event == null){
        event = new Event();            
        map.put(key, event);

        // Map already contains this key, just adjust the numbers.
    }else{
        // Modify the object fields
    }*/
    //}

    // Make it thread-safe using CHM
    Event newEvent = new Event();
    Event existingEvent= map.putIfAbsent(key, newEvent); 

    if(existingEvent!=null && existingEvent!=newEvent){
        // Modify the object fields
}       

private void saveEntries(){

    Map<String, List<Event>> engineCalls = null;
    try {           

        engineCalls = new HashMap<String, List<Event>>();
        List<Event> events = null;

// How can I achieve therad safety here w/o applying any lock?
        //synchronized(lock){
            if(!vendor1Calls.isEmpty()){
                events = new ArrayList<Event>();
                events.addAll(vendor1Calls.values());
                engineCalls.put(vendor1, events);
                vendor1Calls.clear();
            }
            if(!vendor2Calls.isEmpty()){
                events = new ArrayList<Event>();
                events.addAll(vendor2Calls.values());
                engineCalls.put(vendor2, events);
                vendor2Calls.clear();
            }
        //}

// logICalls() saves the events in the DB.          
        DBHandle.logCalls(engineCalls);
    } catch (Throwable t) {         
    } finally {
        if(engineCalls!=null){
            engineCalls.clear();
        }                       
    }   
}       

}

preguntado el 24 de agosto de 12 a las 20:08

Acepta la respuesta haciendo clic en la marca de verificación gris a la izquierda de una respuesta. Sin embargo, no acepte la respuesta si no resuelve su problema, a pesar de la presión de los compañeros. -

Sí, te sugiero que vayas a stackoverflow.com/users/1052348/bluesky?tab=preguntas, haga clic en cada enlace y haga clic en la marca de verificación junto a la respuesta que mejor resolvió su problema. PD: puede aceptar una respuesta que usted mismo hizo a su propia pregunta. -

cruzado aquí, y puede hacerlo atómico usando solo las características de CHM. -

Sé que esta es una vieja pregunta, pero no entiendo por qué estás usando un mapa para algo que parece inherentemente una cola en primer lugar. Puede agregar entradas de registro a una cola concurrente y luego, al guardar las entradas, observe el tamaño de la cola y elimine esa cantidad del frente de la cola y escríbalas. Debido a que solo mira el tamaño una vez, no importa si se agregan más mientras los guarda; quedarán para la próxima vez. Lo siento si no he entendido bien tu propósito. -

5 Respuestas

Sin embargo, cuando lo leo y luego elimino todas las entradas con el método clear(), no quiero que se escriba ningún otro subproceso en el mapa mientras estoy en el proceso de leer el contenido del mapa y luego eliminarlo.

Creo que lo que estás tratando de decir es que realmente no te importa bloquear estrictamente los mapas. En cambio, lo único que realmente le importa es la pérdida de cualquier entrada de registro entre vendedor1Calls.values() y vendedor1Calls.clear(), ¿correcto?

En ese caso, me imagino que puedes reemplazar

events.addAll(vendor1Calls.values());
vendor1Calls.clear();

con esto en saveEntries:

for (Iterator<Event> iter = vendor1Calls.values().iterator(); iter.hasNext(); ) {
    Event e = iter.next();
    events.add(e);
    iter.remove();
}

De esa manera, solo elimina los eventos que ya agregó a la lista de eventos. Todavía puede escribir en los mapas de Vendor1Calls mientras saveEntries() aún se está ejecutando, pero el iterador omite los valores agregados.

Respondido 24 ago 12, 21:08

En realidad, puede iterar sobre los valores agregados mientras tanto, pero no hay garantías. - Marko Topolnik

Gracias por la respuesta. ¿Cómo el uso de Iterator lo hace seguro para subprocesos? ¿Cómo puedo asegurarme de que no haya 2 subprocesos al mismo tiempo, iterando a través de los valores () de los mapas y mientras uno realiza: Evento e = iter.next (); el otro subproceso no funciona: iter.remove(); ? Además, mi objetivo era bloquear todo el mapa que ningún otro subproceso escribe mientras estoy en el proceso de lectura del mapa, b/c, ¿qué sucedería con las entradas agregadas al mapa cuando estoy en el proceso de iteración? a través de ellos? - cielo azul

Supuse por el hecho de que, dado que usó un temporizador para llamar a saveEntries(), que saveEntries() es privado y que no tiene otro código, no tendrá más de un subproceso iterando sobre las llamadas de proveedor 1 o las llamadas de proveedor 2 y posiblemente llamar a eliminar en ellos. - kevin jin

Los que se escriben mientras se iteran pueden guardarse o no ("los iteradores y las enumeraciones devuelven elementos que reflejan el estado de la tabla hash en algún momento o desde la creación del iterador/enumeración"), pero hay una cosa para seguro: no se perderá absolutamente ninguna entrada, porque las que se eliminan llamando a iter.remove() en saveEntries() ya se han agregado a los eventos ArrayList. - kevin jin

tenga en cuenta que iter.remove() elimina el elemento devuelto por iter.next(). no espere que se agregue algo en el lugar ocupado por su posición actual en el iterador y que iter.remove() lo elimine por error. - kevin jin

Sin ninguna sincronización externa, no puede lograr esto con un CHM. Las vistas de iterador devueltas son débilmente consistentes, lo que significa que el contenido del mapa puede cambiar mientras se está iterando sobre él.

Parece que necesitarías usar un Collections.synchronizedMap para obtener la funcionalidad que está buscando.

Editar para hacer mi punto más claro:

Para lograr esto con un synchronizedMap Primero tendrías que synchronize en el Mapa y luego puede iterar o copiar los contenidos en otro mapa y luego borrar.

Map map = Collections.synchronizedMap(new HashMap());

public void work(){
  Map local = new HashMap();
  synchronized(map){
     local.putAll(map);
     map.clear();
  }
  //do work on local instance 
}

En vez de local ejemplo, como mencioné, usted puede iterate + remove similar a la respuesta de @Kevin Jin.

Respondido 24 ago 12, 22:08

syncronizedMapLos iteradores de son inútiles para el trabajo ya que se desmoronan ante los cambios simultáneos en el mapa. Por otro lado, no hay nada de malo en que el iterador CHM capture las entradas posteriores, incluso podría ser mejor ya que se guardarán y eliminarán más entradas del mapa. - Marko Topolnik

@Marko Topolnik Estaba hablando en términos de atomicidad. Debería haber hecho más obvio que él tendría que synchronize en el propio Mapa. Obviamente, esto tiene el efecto secundario de la degradación del rendimiento, pero si no necesita que se actualicen los subprocesos mientras otro subproceso itera y luego se borra, no habría otra forma de hacerlo. Sin embargo, ese puede no ser el caso de sus necesidades. - juan vint

@JohnVint: dado que Iterator no me da una instantánea, ¿es posible que termine con un bucle que nunca termina? ¡B/c como subprocesos en realidad podría agregar entradas al mapa mucho más rápido de lo que las elimino! Entonces, ¿hay alguna forma de obtener una instantánea o hacerla atómica de alguna manera sin usar bloqueos? - cielo azul

En teoría puede, pero en la práctica es poco probable. Puede acelerar obteniendo el tamaño antes de iterar y si un conteo excede, puede romper antes. Dicho esto, un bucle infinito no es algo de lo que me preocuparía. juan vint

Gracias a todos por el tiempo y las respuestas. Muy útil. Lo pensaré... parece que hacerlo atómico no es una opción aquí. Elegiré la apuesta. sincronizadoMap o simplemente iterando a través de. Gracias de nuevo. - cielo azul

Se muestra una versión atómica de este ejemplo. en este hilo (usando solo las funciones en ConcurrentMap).

Respondido 25 ago 12, 13:08

Mi mejor sugerencia sería usar un LeerEscribirBloquear pero como dices específicamente que no quieres usar ningún bloqueo (por cierto ConcurrentHashMap probablemente los usará internamente) puede intentar lo siguiente.

Usa una Referencia atómica a cada uno de sus mapas y cuando llegue el momento de registrar su contenido use getAndSet para reemplazar el mapa antiguo por uno nuevo y vacío.

Ahora tiene un mapa con uso exclusivo que puede iterar y borrar todo lo que quiera. Desafortunadamente, hay un pequeño problema (que se eliminará con el uso de un candado) y es si otro subproceso está en proceso de agregarse al mapa en el momento en que lo cambia por uno vacío. Quizás podría agregar un retraso en este punto con la esperanza de esperar lo suficiente para que el otro subproceso termine lo que estaba haciendo. Tal vez hay alguna funcionalidad interna de un ConcurrentHashMap puede usar para esperar hasta que todos hayan terminado con él.

Respondido 25 ago 12, 23:08

El siguiente código usa un mapa persistente de Java funcional proyecto. Utiliza más memoria, pero es (AFAIK :) seguro de usar por múltiples subprocesos. El único valor mutable en el AtomicReference y se actualiza con un comparar y configurar. El mapa y el evento son inmutabley, por lo tanto, seguro para subprocesos. Además, en lugar de borrar el mapa, reemplazo la referencia a él.

import fj.F;
import fj.Ord;
import fj.data.TreeMap;

import java.util.*;
import java.util.concurrent.atomic.AtomicReference;

public class Logging
{
    // Event is immutable
    private static class Event
    {
        // updates are done by creating new values
        Event update(String key)
        {
            return new Event();
        }
    }

    // Refactored code pertaining to one vendor into a separate class.
    private static class EngineLogger
    {
        public final String vendor;
        private final AtomicReference<TreeMap<String, Event>> mapRef =
                new AtomicReference<TreeMap<String, Event>>(TreeMap.<String, Event>empty(Ord.stringOrd));

        private EngineLogger(String vendor)
        {
            this.vendor = vendor;
        }

        public void log(String service, String method)
        {
            final String key = service + "." + method;
            boolean updated = false;
            while (! updated)
            {
                // get the current value of the map
                TreeMap<String, Event> currMap = mapRef.get();

                // create an updated value of the map, which is the current map plus info about the new key
                TreeMap<String, Event> newMap = currMap.update(key, new F<Event, Event>()
                {
                    @Override
                    public Event f(Event event)
                    {
                        // Modify the object fields of event, if the map contains the key
                        return event.update(key);
                    }
                    // create a new event if the map does not contain the key
                }, new Event());

                // compare-and-set the new value in .. repeat until this succeeds
                updated = mapRef.compareAndSet(currMap, newMap);
            }
        }

        public List<Event> reset()
        {
            /* replace the reference with a new map */
            TreeMap<String, Event> oldMap = mapRef.getAndSet(TreeMap.<String, Event>empty(Ord.stringOrd));

            /* use the old map to generate the list */
            return new ArrayList<Event>(oldMap.toMutableMap().values());
        }
    }

    private static Logging instance;
    private static long delay = 5 * 60 * 1000;
    private final Timer timer;

    private final EngineLogger vendor1 = new EngineLogger("vendor1");
    private final EngineLogger vendor2 = new EngineLogger("vendor2");

    private Logging()
    {
        timer = new Timer();
        timer.schedule(new TimerTask()
        {
            public void run()
            {
                try
                {
                    saveEntries();
                }
                catch (Throwable t)
                {
                    timer.cancel();
                    timer.purge();
                }
            }
        }, 0, delay);
    }

    public static synchronized Logging getInstance()
    {
        if (instance == null)
        {
            instance = new Logging();
        }
        return instance;
    }

    public void log(String engine, String service, String method)
    {
        if (vendor1.vendor.equals(engine))
        {
            vendor1.log(service, method);
        }
        else if (vendor2.vendor.equals(engine))
        {
            vendor2.log(service, method);
        }
    }

    private void saveEntries()
    {
        Map<String, List<Event>> engineCalls = new HashMap<String, List<Event>>();
        engineCalls.put(vendor1.vendor, vendor1.reset());
        engineCalls.put(vendor2.vendor, vendor2.reset());
        DBHandle.logCalls(engineCalls);
    }
}

Respondido 28 ago 12, 10:08

Aunque puede no ser adecuado para mi propósito debido al uso de memoria, es muy útil. Muchas gracias por compartir. - cielo azul

@blueSky Pensando más, me doy cuenta de que esta solución escala mal. los CAS in log el método fallará incluso cuando se modifiquen diferentes subprocesos Events para diferentes claves. - Tomás Binil

Gracias por la actualización. No pude usarlo debido a un problema de memoria. Siempre es bueno saber lo que hay por ahí :) - cielo azul

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.