Caché segura de subprocesos de un objeto en java


Digamos que tenemos un objeto CountryList en nuestra aplicación que debería devolver la lista de países. La carga de países es una operación pesada, por lo que la lista debe almacenarse en caché.

Requisitos adicionales:

  • CountryList debe ser thread-safe
  • CountryList debe cargar lazy (solo bajo demanda)
  • CountryList debería apoyar la invalidación de la caché
  • CountryList debe optimizarse teniendo en cuenta que la caché se invalidará muy rara vez

, se me ocurrió la siguiente solución:

public class CountryList {
    private static final Object ONE = new Integer(1);

    // MapMaker is from Google Collections Library    
    private Map<Object, List<String>> cache = new MapMaker()
        .initialCapacity(1)
        .makeComputingMap(
            new Function<Object, List<String>>() {
                @Override
                public List<String> apply(Object from) {
                    return loadCountryList();
                }
            });

    private List<String> loadCountryList() {
        // HEAVY OPERATION TO LOAD DATA
    }

    public List<String> list() {
        return cache.get(ONE);
    }

    public void invalidateCache() {
        cache.remove(ONE);
    }
}

¿Qué piensas al respecto? ¿Ves algo malo en ello? ¿Hay otra manera de hacerlo? ¿Cómo puedo hacerlo mejor? Debo buscar totalmente otra solución en estos casos?

Gracias.

Author: Igor Mukhin, 2010-09-03

11 answers

Google collections en realidad suministra solo la cosa para este tipo de cosas: Proveedor

Su código sería algo así como:

private Supplier<List<String>> supplier = new Supplier<List<String>>(){
    public List<String> get(){
        return loadCountryList();
    }
};


// volatile reference so that changes are published correctly see invalidate()
private volatile Supplier<List<String>> memorized = Suppliers.memoize(supplier);


public List<String> list(){
    return memorized.get();
}

public void invalidate(){
    memorized = Suppliers.memoize(supplier);
}
 31
Author: Gareth Davis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-03-19 23:53:00

Gracias a todos los chicos , especialmente al usuario " gid" que dio la idea.

Mi objetivo era optimizar el rendimiento de la operación get() considerando que la operación invalidate() se llamará muy rara.

Escribí una clase de prueba que inicia 16 subprocesos, cada uno llamando a la operación get () un millón de veces. Con esta clase perfilé alguna implementación en mi maschine de 2 núcleos.

Resultados de las pruebas

Implementation              Time
no synchronisation          0,6 sec
normal synchronisation      7,5 sec
with MapMaker               26,3 sec
with Suppliers.memoize      8,2 sec
with optimized memoize      1,5 sec

1) "Sin sincronización" no es hilo seguro, pero nos da el mejor rendimiento que podemos comparar.

@Override
public List<String> list() {
    if (cache == null) {
        cache = loadCountryList();
    }
    return cache;
}

@Override
public void invalidateCache() {
    cache = null;
}

2) "Sincronización normal" -rendimiento bastante bueno, implementación estándar obvia

@Override
public synchronized List<String> list() {
    if (cache == null) {
        cache = loadCountryList();
    }
    return cache;
}

@Override
public synchronized void invalidateCache() {
    cache = null;
}

3) "con MapMaker" - muy mal rendimiento.

Ver mi pregunta en la parte superior para el código.

4) " con los Proveedores.memoize " - buen rendimiento. Pero como el rendimiento de la misma " Sincronización normal "tenemos que optimizarlo o simplemente utilizar la"Sincronización normal".

Ver el respuesta del usuario "gid" para el código.

5) "con memoize optimizado" - el rendimiento comparable a "no sync"-implementación, pero hilo seguro uno. Este es el que necesitamos.

La propia clase de caché: (Las interfaces de proveedor utilizadas aquí son de la Biblioteca de Colecciones de Google y solo tiene un método get (). véase http://google-collections.googlecode.com/svn/trunk/javadoc/com/google/common/base/Supplier.html)

public class LazyCache<T> implements Supplier<T> {
    private final Supplier<T> supplier;

    private volatile Supplier<T> cache;

    public LazyCache(Supplier<T> supplier) {
        this.supplier = supplier;
        reset();
    }

    private void reset() {
        cache = new MemoizingSupplier<T>(supplier);
    }

    @Override
    public T get() {
        return cache.get();
    }

    public void invalidate() {
        reset();
    }

    private static class MemoizingSupplier<T> implements Supplier<T> {
        final Supplier<T> delegate;
        volatile T value;

        MemoizingSupplier(Supplier<T> delegate) {
            this.delegate = delegate;
        }

        @Override
        public T get() {
            if (value == null) {
                synchronized (this) {
                    if (value == null) {
                        value = delegate.get();
                    }
                }
            }
            return value;
        }
    }
}

Ejemplo de uso:

public class BetterMemoizeCountryList implements ICountryList {

    LazyCache<List<String>> cache = new LazyCache<List<String>>(new Supplier<List<String>>(){
        @Override
        public List<String> get() {
            return loadCountryList();
        }
    });

    @Override
    public List<String> list(){
        return cache.get();
    }

    @Override
    public void invalidateCache(){
        cache.invalidate();
    }

    private List<String> loadCountryList() {
        // this should normally load a full list from the database,
        // but just for this instance we mock it with:
        return Arrays.asList("Germany", "Russia", "China");
    }
}
 16
Author: Igor Mukhin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-09-06 06:18:16

Siempre que necesite almacenar algo en caché, me gusta usar el patrón Proxy . Hacerlo con este patrón ofrece separación de preocupaciones. Su original el objeto puede estar relacionado con la carga lenta. Su objeto proxy (o tutor) puede ser responsable de la validación de la caché.

En detalle:

  • Defina una clase CountryList de objeto que sea segura para subprocesos, preferiblemente usando bloques de sincronización u otros bloqueos de semáforos.
  • Extraer la interfaz de esta clase en una interfaz CountryQueryable.
  • Defina otro objeto, CountryListProxy, que implemente el CountryQueryable.
  • Solo permite que CountryListProxy sea instanciado, y solo permite que sea referenciado a través de su interfaz.

Desde aquí, puede insertar su estrategia de invalidación de caché en el objeto proxy. Ahorre el tiempo de la última carga y, en la siguiente solicitud para ver los datos, compare la hora actual con la hora de la caché. Definir un nivel de tolerancia, donde, si ha pasado demasiado tiempo, los datos se vuelven a cargar.

En cuanto a la Carga perezosa, consulte aquí.

Ahora para un buen código de ejemplo de down-home:

public interface CountryQueryable {

    public void operationA();
    public String operationB();

}

public class CountryList implements CountryQueryable {

    private boolean loaded;

    public CountryList() {
        loaded = false;
    }

    //This particular operation might be able to function without
    //the extra loading.
    @Override
    public void operationA() {
        //Do whatever.
    }

    //This operation may need to load the extra stuff.
    @Override
    public String operationB() {
        if (!loaded) {
            load();
            loaded = true;
        }

        //Do whatever.
        return whatever;
    }

    private void load() {
        //Do the loading of the Lazy load here.
    }

}

public class CountryListProxy implements CountryQueryable {

    //In accordance with the Proxy pattern, we hide the target
    //instance inside of our Proxy instance.
    private CountryQueryable actualList;
    //Keep track of the lazy time we cached.
    private long lastCached;

    //Define a tolerance time, 2000 milliseconds, before refreshing
    //the cache.
    private static final long TOLERANCE = 2000L;

    public CountryListProxy() {
            //You might even retrieve this object from a Registry.
        actualList = new CountryList();
        //Initialize it to something stupid.
        lastCached = Long.MIN_VALUE;
    }

    @Override
    public synchronized void operationA() {
        if ((System.getCurrentTimeMillis() - lastCached) > TOLERANCE) {
            //Refresh the cache.
                    lastCached = System.getCurrentTimeMillis();
        } else {
            //Cache is okay.
        }
    }

    @Override
    public synchronized String operationB() {
        if ((System.getCurrentTimeMillis() - lastCached) > TOLERANCE) {
            //Refresh the cache.
                    lastCached = System.getCurrentTimeMillis();
        } else {
            //Cache is okay.
        }

        return whatever;
    }

}

public class Client {

    public static void main(String[] args) {
        CountryQueryable queryable = new CountryListProxy();
        //Do your thing.
    }

}
 5
Author: Mike,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-09-03 15:44:47

No estoy seguro de para qué es el mapa. Cuando necesito un objeto perezoso en caché, generalmente lo hago así:

public class CountryList
{
  private static List<Country> countryList;

  public static synchronized List<Country> get()
  {
    if (countryList==null)
      countryList=load();
    return countryList;
  }
  private static List<Country> load()
  {
    ... whatever ...
  }
  public static synchronized void forget()
  {
    countryList=null;
  }
}

Creo que esto es similar a lo que estás haciendo, pero un poco más simple. Si necesitas el mapa y el que has simplificado para la pregunta, está bien.

Si lo quieres thread-safe, debes sincronizar el get y el forget.

 1
Author: Jay,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-09-03 14:04:25

¿Qué piensas al respecto? ¿Ves algo malo en ello?

Bleah-está utilizando una estructura de datos compleja, MapMaker, con varias características (acceso a mapas, acceso compatible con la concurrencia, construcción diferida de valores, etc.) debido a una sola característica que está buscando (creación diferida de una sola construcción-objeto costoso).

Si bien reutilizar el código es un buen objetivo, este enfoque agrega sobrecarga y complejidad adicionales. Además, engaña el futuro mantenedores cuando ven una estructura de datos de mapa allí en el pensamiento de que hay un mapa de claves / valores allí cuando realmente hay solo 1 cosa (lista de países). La simplicidad, la legibilidad y la claridad son clave para el mantenimiento futuro.

Hay otra manera de hacerlo? ¿Cómo puedo hacerlo mejor? Debo buscar totalmente otra solución en estos casos?

Parece que estás tras la carga perezosa. Mira las soluciones a otras preguntas tan perezosas. Por ejemplo, este cubre el enfoque clásico de doble comprobación (asegúrese de que está utilizando Java 1.5 o posterior):

¿Cómo resolver la Declaración" El bloqueo doble comprobado está roto " en Java?

En lugar de simplemente repetir el código de solución aquí, creo que es útil leer la discusión sobre la carga perezosa a través de doble verificación allí para aumentar su base de conocimientos. (lo siento si eso sale como pomposo - solo tratando de enseñar a pescar en lugar de alimentar bla, bla, bla ...)

 1
Author: Bert F,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:10:22

Hay una biblioteca por ahí (de atlassian) - una de las clases util llamada LazyReference. LazyReference es una referencia a un objeto que se puede crear perezosamente (en primer lugar get). es guarenteed thread seguro, y el init también es guarenteed que solo ocurra una vez - si dos threads llaman a get() al mismo tiempo, un thread calculará, el otro thread bloqueará wait.

Ver un código de ejemplo :

final LazyReference<MyObject> ref = new LazyReference() {
    protected MyObject create() throws Exception {
        // Do some useful object construction here
        return new MyObject();
    }
};

//thread1
MyObject myObject = ref.get();
//thread2
MyObject myObject = ref.get();
 1
Author: Chii,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-09-03 14:17:04

Sus necesidades parecen bastante simples aquí. El uso de MapMaker hace que la implementación sea más complicada de lo que tiene que ser. Todo el lenguaje de bloqueo doble comprobado es difícil de corregir, y solo funciona en 1.5+. Y para ser honesto, está rompiendo una de las reglas más importantes de la programación:

La optimización prematura es la raíz de todo mal.

El modismo de bloqueo doble comprobado intenta evitar el costo de la sincronización en el caso de que la caché ya esté cargar. Pero es que la sobrecarga realmente causando problemas? ¿Vale la pena el costo de un código más complejo? Yo digo que asumas que no es hasta que el perfil te diga lo contrario.

Aquí hay una solución muy simple que no requiere código de terceros (ignorando la anotación JCIP). Supone que una lista vacía significa que la caché no se ha cargado todavía. También evita que el contenido de la lista de países se escape al código del cliente que podría modificar la lista devuelta. Si esto no es un preocupación por usted, usted podría eliminar la llamada a las Colecciones.unmodifiedList().

public class CountryList {

    @GuardedBy("cache")
    private final List<String> cache = new ArrayList<String>();

    private List<String> loadCountryList() {
        // HEAVY OPERATION TO LOAD DATA
    }

    public List<String> list() {
        synchronized (cache) {
            if( cache.isEmpty() ) {
                cache.addAll(loadCountryList());
            }
            return Collections.unmodifiableList(cache);
        }
    }

    public void invalidateCache() {
        synchronized (cache) {
            cache.clear();
        }
    }

}
 1
Author: wolfcastle,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-03 23:00:14

Esto me parece bien (supongo que MapMaker es de Google collections?) Idealmente, no necesitaría usar un mapa porque realmente no tiene claves, pero como la implementación está oculta a las personas que llaman, no veo esto como un gran problema.

 0
Author: Mike Q,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-09-03 13:36:59

Esta es una manera sencilla de usar las cosas de ComputingMap. Solo necesita una implementación simple muerta donde todos los métodos están sincronizados, y debería estar bien. Esto obviamente bloqueará el primer hilo que lo golpee (obteniéndolo), y cualquier otro hilo que lo golpee mientras el primer hilo carga la caché (y lo mismo de nuevo si alguien llama a la cosa invalidateCache-donde también debe decidir si la invalidateCache debe cargar la caché de nuevo, o simplemente anularla, dejando en la recepción de nuevo bloque), pero luego todos los hilos deben pasar bien.

 0
Author: stolsvik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-09-03 13:57:44

Use el modismo de inicialización bajo demanda

public class CountryList {
  private CountryList() {}

  private static class CountryListHolder {
    static final List<Country> INSTANCE = new List<Country>();
  }

  public static List<Country> getInstance() {
    return CountryListHolder.INSTANCE;
  }

  ...
}
 0
Author: helpermethod,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-09-03 14:26:59

Siga la solución de Mike arriba. Mi comentario no se formateó como se esperaba... :(

Tenga cuidado con los problemas de sincronización en operationB, especialmente porque load() es lenta:

public String operationB() {
    if (!loaded) {
        load();
        loaded = true;
    }

    //Do whatever.
    return whatever;
}

Podrías arreglarlo de esta manera:

public String operationB() {
    synchronized(loaded) {
        if (!loaded) {
            load();
            loaded = true;
        }
    }

    //Do whatever.
    return whatever;
}

Asegúrese de sincronizar SIEMPRE en cada acceso a la variable cargada.

 0
Author: romacafe,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-09-03 16:08:24