Java: Implementación de una caché con Referencias Débiles

Últimamente me ha surgido una necesidad que hasta ahora no se me había presentado.
Os comento de qué se trata:

Estaba desarrollando una aplicación web (jsf, seam, etc.), en unos de las páginas se generaba un listado en el que se muestra datos sobre una serie de documentos.

Existe la posibilidad de descargar estos documentos, que no son más que archivos pdfs, a través de una aplicación cliente que a su vez se despliega con Java Web Start con JNLP en la misma aplicación web.
El problema es el siguiente:

El usuario X a través de la aplicación ve el listado, pero el JNLP no comparte la misma sesión que el navegador, luego no podemos acceder a dicho listado, ya que se trata de un objeto visible en el conexto Sesión.
Una primera solución consistiría en pasar los distintos identificadores de los documentos listados al JNLP a través de parámetros en la URL. Pero existe el problema de que el listado sea demasiado largo y la URL resultante sea demasiado larga, problema con caracteres no aptos para una URL, etc.  Podemos jugárnosla, optar por esta “solución” y rezar para que nunca se produzca esa situación… no mola, sigamos pensando y busquemos otra solución.
Posteriormente podemos pensar en identificar el listado y mantener los elementos relacionados en una caché. Caché que no se alojará a nivel de Sesión, sino en el conexto de la Aplicación (ServletContext), para que esté visible desde todas las sesiones (usuario web y usuario jnlp).

Pero, ¿cómo implementamos esta caché?
Usemos un simple HashMap:

El primer usuario entra, hace un listado, se introduce en la caché el listado.
El mismo usuario genera otro listado, se introduce en la caché.
Entra otro usuario, lo mismo, un nuevo elemento a la caché.
Trás introducir cinco mil elementos en la caché vemos que la aplicación ya no puede más y dice ‘¡¡ hasta aquí hemos llegado !!’.
¿Qué ha pasado? Pues que la caché ha almacenado muchísimos listados y se ha ‘comido’ muchísima memoria.

¿Cómo podríamos solucionarlo? Establezcamos un tamaño máximo de cacheo, lo que nos llevará a proponer una determinada política de reemplazo (aleatoria, LRU, LFU, FIfo). Demasiadas cosas para pensar, y teniendo en cuenta las leyes de Murphy ‘Si algo puede fallar, fallará’, así que mejor sigo investigando otra forma de implementar la caché.

Y ¿se podría implementar la caché de tal forma que el recolector de basura de Java vaya borrando sus elementos cuando se esté consumiento demasiada memoria? Esto nos lleva al concepto de “Referencia débil”.

Recordemos los tipos de referencias en Java: Fuertes, débiles, blandas y fantasma. En la práctica las débiles y las blandas son tratadas igual por la Máquina Virtual Java (no sigo explicando esto porque no es el objetivo de este post).

Cuando el recolector de basura se ejecuta y va recorriendo los distintos objetos, si estos objetos son apuntados por referencias débiles se eliminan, tal y como suena, esto no ocurre con las referencias fuertes.
¿Cómo podemos aprovechar esto en nuestra caché? Pues a través de la clase WeakHashMap (véase java.util.WeakHashMap). Esta clase implementa java.util.Map, pero las keys se almacenan como objetos con referencias débiles. Con esto se dejará la responsabilidad de vaciar la caché al recolector de basura.
Pues nada más, con esto hemos conseguido implementar una caché visible desde las distintas sesiones en una aplicación y gestionada por el Recolector de Basura.

A continuación os pongo un pequeño código de ejemplo para que podais ver la diferencia de funcionamiento entre un HashMap y un WeakHashMap.

import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;

public class Prueba {
	public static void main(String[] args) {
		Map map = new HashMap();
		Map weakMap = new WeakHashMap();
		imprime("map", map);
		imprime("weakMap", weakMap);
		for (int i = 0; i < 50; i++) {
			System.out.println("=======================n=======================");
			System.out.println("Insertando: " + i);
			System.out.println("Insertando: " + i + "_key");
			map.put("" + i, "" + i);
			map.put("" + i + "_key", "" + i);
			weakMap.put("" + i, "" + i);
			weakMap.put("" + i + "_key", "" + i);
			imprime("map", map);
			imprime("weakMap", weakMap);
			if (map.keySet().size() != weakMap.keySet().size()) {
				System.out.println("DISTINTO TAMAÑO !!!");
			}
			System.out.println("Eliminando: " + i);
			map.remove("" + i);
			weakMap.remove("" + i);
			imprime("map", map);
			imprime("weakMap", weakMap);
		}
	}

	private static void imprime(String texto, Map map) {
		System.out.println("n-----------" + texto + "-----------");
		System.out.println("Size: " + map.keySet().size());
		System.out.println("Map:" + map);
		System.out.println("------------------------------n");
	}
}

Por cierto, quien quiera seguir investigando las referencias débiles puede probar con la clase java.lang.ref.WeakReference.

Saludos.

Comentarios

  1. Te recomiendo utilizar SoftReference en vez de WeakReference. La diferencia fundamental que veo es que softreference dura mientras haya memoria y es el garbage el que se encarga de eliminar lo que no es util, para colmo gestiona las mas utilizadas y solo libera las recientes, lo malo no tienes una clase que lo haga y debes crearte un hashmap que gestione todo.

Comments are closed.