supongamos que tenemos un CountryList objeto en nuestra aplicación que debe devolver la lista de países. La carga de los países es una operación pesada, por lo que la lista debe estar en la memoria caché.

Requisitos adicionales:

  • CountryList debe ser seguro para subprocesos
  • CountryList debe de carga diferida (sólo en la demanda)
  • CountryList debe apoyar la invalidación de la caché
  • CountryList debe ser optimizado teniendo en cuenta que la memoria caché puede ser invalidado 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é piensa usted acerca de esto? ¿Ves algo malo acerca de ti? Hay otra forma de hacerlo? ¿Cómo puedo hacerlo mejor? Debo buscar totalmente otra solución en estos casos?

Gracias.

  • No estoy convencido de que este es thread-safe. ¿Qué pasa si dos hilos de llamada invalidateCache() simultáneamente o uno está llamando a lista() al mismo tiempo, otra de las llamadas invalidateCache()?
  • MapMaker devuelve un hilo de seguridad implementationi de la interfaz de Mapa. makeComputingMap() realiza el cálculo de forma atómica (google-collections.googlecode.com/svn/trunk/javadoc/com/google/…)
  • ¿quieres recibir una respuesta?
  • Davis, he aceptado mi propia respuesta.
  • Integer.valueOf(1) se presenta una versión en caché de la clave.
InformationsquelleAutor Igor Mukhin | 2010-09-03

11 Comentarios

  1. 33

    google colecciones en realidad suministros sólo la cosa para este tipo de cosas: Proveedor

    El código sería algo 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);
    }
    • uno agradable. Gracias. Acabo de probarlo. Esto realmente mejora el rendimiento.
    • ¿Qué es un ‘violar’ de referencia :-)?
    • La palabra clave es volatile.
    • doh… fijo ta.
    • Además, este código no compila. get() necesita un return.
  2. 17

    Gracias a todos chicos, especialmente para el usuario «gid» que le dio la idea.

    Mi objetivo era optimizar el rendimiento de la get() de la operación teniendo en cuenta la invalidate() operación será llamado muy raro.

    Escribí una prueba de clase que comienza el 16 de hilos, cada llamada a get()-la Operación de un millón de veces. Con esta clase perfila algunos de implementación en mis 2-core de maschine.

    Los 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) «No hay sincronización» no es thread-safe, pero nos da el mejor rendimiento que se pueden comparar a.

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

    2) «Normal de sincronización» – muy buena actuación, estándar obviedad aplicación

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

    3) «con MapMaker» – un muy pobre desempeño.

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

    4) «con los Proveedores.memoize» – un buen rendimiento. Pero como el rendimiento de la misma «Normal de sincronización» tenemos que optimizar o simplemente el uso de la «Normal de sincronización».

    Ver la respuesta del usuario «gid» para el código.

    5) «con la optimización de memoize» – el performnce comparable a «no»sincronización de implementación, pero thread-safe uno. Este es el que necesitamos.

    La caché de clase en sí:
    (El Proveedor de interfaces que se usa aquí es de Google Colecciones de la Biblioteca y sólo tiene un método get(). ver 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");
        }
    }
    • ¿puede por favor explicar por qué la versión optimizada es más rápido? también, intenta con este ejemplo stackoverflow.com/a/3637441/638670 La diferencia entre esta versión y su versión sincronizada es que sincronizar todo el método, mientras que todo lo que usted necesita es el acceso sincronizado a la caché de objetos.
  3. 5

    Cuando necesito caché de algo, me gusta usar la Modelo Proxy.
    Con este patrón ofertas separación de preocupaciones. Su original
    objeto puede ser que se trate con carga diferida. Su proxy (o tutor) objeto
    puede ser responsable de la validación de la memoria caché.

    En detalle:

    • Definir un objeto CountryList clase que es thread-safe, preferentemente, a través de bloques de sincronización u otros semaphore bloqueos.
    • Extracto de esta clase de la interfaz en una CountryQueryable interfaz.
    • Definir otro objeto, CountryListProxy, que implementa el CountryQueryable.
    • Sólo permiten la CountryListProxy ser instanciado, y sólo permitir que se hace referencia
      a través de su interfaz.

    Desde aquí, usted puede insertar su invalidación de caché de la estrategia en el objeto proxy. Ahorrar tiempo en el momento de la última carga, y a la siguiente solicitud para ver los datos, comparar el tiempo actual al tiempo de caché. Definir un nivel de tolerancia, donde, si ha transcurrido mucho tiempo, los datos se vuelve a cargar.

    Tan lejos como la Carga Perezosa, consulte aquí.

    Ahora un poco de buen hogar código de ejemplo:

    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.
    }
    }
    • ¿puede dar un ejemplo de código?
    • Publicado algunos. 🙂
  4. 1

    No estoy seguro de lo que el mapa es para. Cuando necesito un perezoso, caché de objetos, normalmente lo hago de esta manera:

    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 usted tiene una necesidad de que el mapa y el que usted ha simplificado fuera de la cuestión, está bien.

    Si quieres que sea seguro para subprocesos, debe sincronizar el get y el olvidar.

    • Porque actúan sólo en los datos estáticos. Esta clase no tiene los datos de la instancia. Siempre que una función puede ser estático, puedo hacer es estática. Esto es un poco más eficiente y sirve como documentación para el lector.
  5. 1

    ¿Qué piensa usted acerca de esto? ¿Ves algo malo acerca de ti?

    Bleah – usted está utilizando una compleja estructura de datos, MapMaker, con varias características (mapa de acceso, la simultaneidad de fácil acceso, aplazamiento de la construcción de valores, etc) porque de una sola característica que usted está después (diferido creación de una única construcción-caro objeto).

    Mientras que la reutilización de código es una buena meta, este enfoque añade una sobrecarga adicional y la complejidad. Además, se engaña a futuro mantenedores cuando vemos un mapa de la estructura de datos que hay en el pensamiento de que hay un mapa de claves/valores cuando en realidad sólo hay 1 cosa (la lista de países). Sencillez, legibilidad y la claridad son la clave para el futuro de la capacidad de mantenimiento.

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

    Parece que están después de la carga diferida. Buscar soluciones a otros, de MODO de carga diferida preguntas. Por ejemplo, este cubre el doble clásica-comprobación de enfoque (asegúrese de que está utilizando Java 1.5 o posterior):

    Cómo resolver el problema de la «Doble comprobación de Cierre está Roto» Declaración en Java?

    En lugar de simplemente repetir la solución de código aquí, creo que es útil para leer la discusión acerca de la carga diferida a través de una doble comprobación de allí para hacer crecer su base de conocimientos. (lo siento si que sale como pomposo – tratando de enseñar a pescar, en vez de alimentar bla, bla, bla …)

  6. 1

    Hay una biblioteca que hay (de atlassian) – uno de los util clases llamadas LazyReference. LazyReference es una referencia a un objeto que puede ser perezosamente creado (en el primer get). es avaladas subprocesos, y el init también está garantizado que sólo se dan una vez – si dos hilos de llamadas get() al mismo tiempo, un hilo de cálculo, el otro hilo bloque de esperar.

    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();
    • buena propina, pero esta clase no admite controlado invalidación de caché.
    • ah, eso es cierto.
  7. 1

    Sus necesidades parecen bastante simple aquí. El uso de MapMaker hace que la aplicación más complicado que tiene que ser. El conjunto de la doble marca de bloqueo del lenguaje es difícil de conseguir, y sólo funciona en 1.5+. Y para ser honesto, es romper una de las reglas más importantes de la programación:

    Optimización prematura es la raíz de
    todos los males.

    La doble marca de bloqueo lenguaje intenta evitar el costo de sincronización en el caso de que la caché está ya cargado. Pero es que la sobrecarga de que realmente está causando problemas? Vale la pena el costo de código más complejo? Yo digo que asumir que no es hasta de perfiles le indique lo contrario.

    Aquí tienes una solución muy sencilla que no requiere de la 3ª parte de código (ignorando la JCIP anotación). Se hace la suposición de que una lista vacía significa que la memoria caché no se ha cargado todavía. También evita que el contenido de la lista de países de escape código de cliente que potencialmente podría modificar la lista devuelta. Si esto no es una preocupación para 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();
    }
    }
    }
  8. 0

    Esto se ve muy bien a mí (supongo MapMaker es de google colecciones?) Idealmente, usted no necesita usar un Mapa porque usted realmente no tiene llaves, pero como la aplicación está oculto de cualquier personas que llaman no veo esto como un gran problema.

  9. 0

    Esta es la forma simple de utilizar la ComputingMap cosas. Usted sólo necesita un muerto sencilla aplicación donde todos los métodos están sincronizados, y usted debería estar bien. Obviamente, esto va a bloquear el primer hilo de golpear (llegar), y cualquier otro hilo de golpearlo mientras que el primer hilo de la carga de la caché (y lo mismo si alguien llama a la invalidateCache cosa – donde también se debe decidir si la invalidateCache se debe cargar en la memoria caché de nuevo, o simplemente nulo fuera, dejando que el primer intento de llegar de nuevo bloque), pero entonces todos los hilos deben ir a través de muy bien.

  10. 0

    Seguimiento a Mike de la solución anterior. Mi comentario no formato como se esperaba… 🙁

    Reloj hacia fuera para los problemas de sincronización en operationB, especialmente desde load() es lento:

    public String operationB() {
    if (!loaded) {
    load();
    loaded = true;
    }
    //Do whatever.
    return whatever;
    }

    Se podría arreglar de esta manera:

    public String operationB() {
    synchronized(loaded) {
    if (!loaded) {
    load();
    loaded = true;
    }
    }
    //Do whatever.
    return whatever;
    }

    Asegúrese de que usted SIEMPRE sincronizar en cada acceso a la carga variable.

    • no se puede sincronizar en una primitiva. Sólo se pueden sincronizar con los objetos.

Dejar respuesta

Please enter your comment!
Please enter your name here