miércoles, 21 de marzo de 2012

Implementación de caché Singleton con Initialization-on-demand holder idiom + SoftReferences

Hace un tiempo hablábamos sobre el "Double-checked locking" como patrón de diseño para reducir la contención en las zonas de exclusión mutua, basado en el uso de variables volatile bajo en JMM vigente desde Java 5. Véase "Double checked locking" por fin funciona en Java

Este patrón tiene sentido en varios escenarios, pero cuando lo que se quiere es implementar el patrón Singleton con "lazy instantiation"... hay una solución mejor en Java: el idioma "Initialization-on-demand holder".

Me he hecho muy fan :-)

Se trata de una solución mucho más sencilla y legible, incluso más eficiente (al menos en teoría), que se basa inteligentemente en el orden de inicialización de clases en la JVM.

A continuación un ejemplo "complejo", juntado con el uso de SoftReferences y de los patrones Delegate y para implementar una sencilla pero eficiente caché en memoria. Para más información y un ejemplo sencillo, el lugar donde está mejor condensada la información es la Wikipedia.

Este ejemplo, "complejo", casi ofende de lo sencillo que resulta :-)

La idea del uso de SoftReferences está tomada de Brian Goetz y su artículo Java theory and practice: Plugging memory leaks with soft references


package org.serverperformance.examples;

import java.lang.ref.SoftReference;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
  * Caché "casera" con lazy-instantiation e implementada
  * como SoftReferences, para optimizar el uso de memoria
  * tanto en el arranque como a la hora de recolección
  * de basura.
  *
  * Implementado mediate:
  *     Patrón Singleton +
  *     Patrón Delegate +
  *     Initialization-on-demand holder idiom +
  *     SoftReferences
  *
  * @see ibm.com/developerworks/java/library/j-jtp01246/
  * @see ibm.com/developerworks/java/library/j-jtp08223/
  * @see en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom
  *
  * @author serverperformance
  */

public final class ExampleCacheMap implements Map<KeysClass, ValuesClass> {

  private static final int INITIAL_CAPACITY = 16;
  private static final float LOAD_FACTOR = 0.75f;
  private static final int CONCURRENCY_LEVEL_FOR_UPDATES = 16;
  
  // Comentarios sobre la cache:
  //
  // 1. Mejor SoftReference<Map<K,V>> que Map<K, SoftReference<V>>,
  //    ver http://www.ibm.com/developerworks/java/library/j-jtp01246
  // 2. Ojo, no usar WeakReference para esto, sí SoftReference
  // 3. No es volatile porque no se utiliza DCL sino
  //    Initialization-on-demand (ver abajo)
  //    así que se establece su inicialización aquí

  private final SoftReference<ConcurrentHashMap<KeysClass, ValuesClass>> implementationSoftReference =
     new SoftReference<ConcurrentHashMap<Class, ValuesClass>>(
      new ConcurrentHashMap<Class, ValuesClass>(
      INITIAL_CAPACITY, LOAD_FACTOR, CONCURRENCY_LEVEL_FOR_UPDATES));

  // Holder para la inicialización de la caché on-demand
  // no es necesario por tanto utilizar un double-check-locking
  // para más info:
  // http://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom

  private static final class SingletonLazyHolder {
    private static final ExampleCacheMap instance =
      new ExampleCacheMap();
  }

  /**
    * Singleton (lazy initialization, Initialization-on-demand Holder)
    */

  public static ExampleCacheMap getInstance() {
    // Es en la primera invocación a este método
    // donde se inicializa el contenido del holder
    // y por tanto la instanciación del Singleton :-)

    return SingletonLazyHolder.instance;
  }

  private Map<KeysClass, ValuesClass> getImplementation() {
    return implementationSoftReference.get();
  }

  private ExampleCacheMap() {
  }

  // Patrón Delegate
  
  @Override
  public Collection<ValuesClass> values() {
    return getImplementation().values();
  }

  @Override
  public int size() {
    return getImplementation().size();
  }

  @Override
  public ValuesClass remove(Object key) {
    return getImplementation().remove(key);
  }

  @Override
  public void putAll(Map<? extends KeysClass,? extends ValuesClass> m){
    getImplementation().putAll(m);
  }

  @Override
  public ValuesClass put(KeysClass key, ValuesClass value) {
    return getImplementation().put(key, value);
  }

  @Override
  public Set<KeysClass> keySet() {
    return getImplementation().keySet();
  }

  @Override
  public boolean isEmpty() {
    return getImplementation().isEmpty();
  }

  @Override
  public ValuesClass get(Object key) {
    return getImplementation().get(key);
  }

  @Override
  public Set<Entry<KeysClass, ValuesClass>> entrySet() {
    return getImplementation().entrySet();
  }

  @Override
  public boolean containsValue(Object value) {
    return getImplementation().containsValue(value);
  }

  @Override
  public boolean containsKey(Object key) {
    return getImplementation().containsKey(key);
  }

  @Override
  public void clear() {
    getImplementation().clear();
  }

  @Override
  public String toString() {
    getImplementation().toString();
    // (Or whatever specific)
  }
}