2010-09-03 9 views
33

digamos que tenemos un objeto CountryList en nuestra aplicación que debe 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é.Caché a prueba de hilos de un objeto en java

requisitos adicionales:

  • CountryList debe ser seguro para subprocesos
  • CountryList debe cargar perezoso (sólo en la demanda)
  • CountryList debe apoyar la invalidación de la caché
  • CountryList debe ser optimizado teniendo en cuenta que el caché se invalidará muy raramente

Vine wi 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é opina sobre esto? ¿Ves algo malo al respecto? ¿Hay otra forma de hacerlo? ¿Cómo puedo hacerlo mejor? ¿Debería buscar una solución totalmente diferente en estos casos?

Gracias.

+2

No estoy seguro de que esto sea seguro para subprocesos. ¿Qué pasa si dos hilos llaman invalidateCache() simultáneamente o uno está llamando a list() al mismo tiempo que otro llama a invalidateCache()? – ChrisH

+1

MapMaker devuelve una implementación thread-safe de la interfaz de Map. makeComputingMap() realiza el cálculo de forma atómica (http://google-collections.googlecode.com/svn/trunk/javadoc/com/google/common/collect/MapMaker.html#makeComputingMap(com.google.common.base.Function)) –

+0

¿quieres aceptar una respuesta? –

Respuesta

15

Gracias usted todos los chicos, especialmente a los usuarios " gid "quién dio la idea.

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

Escribí una clase de prueba que comienza con 16 hilos, cada uno llamando a get() - Operación un millón de veces. Con esta clase, realicé un perfil de 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) "No hay sincronización" no es seguro para subprocesos, 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) "La sincronización normal" - bastante buena performace, estándar de aplicación obviedad

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

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

3) "con Map Maker" - un rendimiento muy pobre.

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

4) "con Suppliers.memoize" - buen rendimiento. Pero como el rendimiento es la misma "sincronización normal", debemos optimizarlo o simplemente usar la "sincronización normal".

Consulte la respuesta del usuario "gid" para ver el código.

5) "con memoize optimizado" - el rendimiento es comparable al de "sin sincronización": implementación, pero seguro para subprocesos. Este es el que necesitamos.

La misma clase de caché: (. Las interfaces Proveedor ha utilizado aquí es de Google colecciones de bibliotecas y tiene sólo 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 del 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"); 
    } 
} 
+1

¿me puede explicar por qué la versión optimizada es más rápida? también, lo intentó con este ejemplo http://stackoverflow.com/a/3637441/638670 La diferencia entre esta versión y su versión sincronizada es que sincroniza todo el método, mientras que todo lo que necesita es acceso sincronizado al objeto de caché . – Nerrve

0

Esto se ve bien para mí (supongo que MapMaker es de las colecciones de google?) Idealmente, no necesitarías usar un Mapa porque realmente no tienes claves pero la implementación está oculta de las personas que llaman. ver esto como un gran problema.

5

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

En detalle:

  • definir una clase CountryList objeto que es thread-safe, preferiblemente usando bloques de sincronización u otros semaphore cerraduras.
  • Extraiga la interfaz de esta clase en una interfaz CountryQueryable.
  • Define otro objeto, CountryListProxy, que implementa el CountryQueryable.
  • Solo permite la creación de instancias de CountryListProxy, y solo permite que se haga referencia a 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 el tiempo de caché. Defina un nivel de tolerancia, donde, si ha pasado demasiado tiempo, los datos se vuelven a cargar.

En cuanto a Lazy Load, consulte here.

Ahora un poco de buena código de ejemplo abajo-hogar:

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. 
    } 

} 
+0

puede proporcionar un código de ejemplo? –

+0

Publicado algunos. :) – Mike

0

Ésta es manera de sencilla de utilizar el material ComputingMap. Solo necesita una implementación muerta simple donde todos los métodos estén sincronizados, y usted debería estar bien.Obviamente, esto bloqueará el primer hilo que lo golpee (lo reciba) y cualquier otro hilo que lo golpee mientras el primer hilo carga el caché (y lo mismo si alguien llama a invalidateCache, donde también debería decidir si invalidateCache debería cargar el almacenar en caché de nuevo, o simplemente anularlo, dejando que el primer intento de obtenerlo bloquee nuevamente), pero luego todos los hilos deberían pasar bien.

1

No estoy seguro de para qué sirve el mapa. Cuando necesito un objeto en caché perezoso, por lo general lo hace 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 necesita un mapa y el que ha simplificado para la pregunta, está bien.

Si lo quieres hilo seguro, debes sincronizar el get y el forget.

+0

@iimuhin: Porque solo actúan sobre datos estáticos. Esta clase no tiene datos de instancia. Siempre que una función puede ser estática, la hago estática. Esto es un poco más eficiente y sirve como documentación para el lector. – Jay

1

¿que opinas sobre él? ¿Ves algo malo al respecto?

Bleah - está utilizando una estructura de datos compleja, Map Maker, con varias características (mapa de acceso, el acceso concurrencia con niños, construcción diferido de los valores, etc.) debido a una sola característica que está después (creación diferida de una único objeto costoso de construcción).

Si bien reutilizar el código es un buen objetivo, este enfoque agrega una sobrecarga adicional y complejidad. Además, confunde a los mantenedores futuros cuando ven una estructura de datos cartográficos que piensen que hay un mapa de claves/valores allí cuando realmente hay una sola cosa (lista de países). La simplicidad, la legibilidad y la claridad son clave para la mantenibilidad futura.

¿Hay alguna otra manera de hacerlo? ¿Cómo puedo hacerlo mejor? ¿Debería buscar una solución totalmente diferente en estos casos?

Parece que estás después de lazy-loading. Mire las soluciones a otras preguntas de carga lenta de SO. Por ejemplo, éste cubre el enfoque clásico de doble comprobación (asegúrese de que está utilizando Java 1.5 o posterior):

How to solve the "Double-Checked Locking is Broken" Declaration in Java?

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

1

Hay una biblioteca (de atlassian) - una de las clases de utilidades llamada LazyReference. LazyReference es una referencia a un objeto que se puede crear de forma perezosa (en el primer intento). se garantiza la seguridad de los subprocesos y también se garantiza que init se produce una sola vez; si dos subprocesos llaman a get() al mismo tiempo, un subproceso calculará, el otro subproceso bloqueará la espera.

see a sample code:

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(); 
+0

buen consejo, pero esta clase no admite la invalidación controlada de caché. –

+0

ah eso es cierto. – Chii

0

Utilice los Initialization on demand holder idiom

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

    ... 
} 
31

colecciones de Google en realidad suministra sólo la cosa sólo para este tipo de cosas: Supplier

Su 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); 
} 
+0

nice one. Gracias. Acabo de probarlo. Esto realmente mejora el rendimiento. –

+0

¿Qué es una referencia 'violada' :-)? – helpermethod

+1

La palabra clave es 'volátil'. –

1

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

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

El idioma de bloqueo con doble verificación trata de evitar el costo de la sincronización en el caso en que la memoria caché ya está cargada. Pero, ¿eso está realmente causando problemas? ¿Vale la pena el costo de un código más complejo? Digo asumir 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 memoria caché aún no se ha cargado. También evita que el contenido de la lista de países se escape a un código de cliente que podría modificar la lista devuelta. Si esto no le preocupa, puede eliminar la llamada a Collections.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(); 
     } 
    } 

} 
0

Haga clic en la solución anterior de Mike. Mi comentario no ha formateado como se esperaba ... :(

Cuidado con problemas de sincronización en operationB, sobre todo porque la carga() es lento:

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

    //Do whatever. 
    return whatever; 
} 

Usted puede fijar de esta manera:

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

    //Do whatever. 
    return whatever; 
} 

asegurarse de que siempre sincroniza en cada acceso a la variable cargado.

+0

no se puede sincronizar en una primitiva. Solo puedes sincronizar en objetos. –

Cuestiones relacionadas