Creacrión de pools de objetos
Construye un pool de objetos en Java
Aumenta el rendimiento, eficiencia y velicidad de tus aplicaciones.
Autor: THOMAS E. DAVIS
Traductor: VICENTE REIG RINCÓN DE ARELLANO
Artículo origianl: http://www.javaworld.com/javaworld/jw-06-1998/jw-06-object-pool_p.html
Resumen
Un pool de objetos es una estructura de datos que permite compartir los objetos que almacena entre varias
aplicaciones o entre los diferentes módulos que las componen. En este caso particular, hablamos de objetos en Java.
De esta manera, conseguimos eliminar el tiempo de carga, pues no necesitamos instanciar o construir esos objetos,
y a la vez reducimos el trabajo del recolector de basura (garbage collector). En este artículo se implementa dicho
pool de objetos y a partir de ahí, un pool de conexiones a una base de datos.
En este artículo, se tiene en cuenta
que el lector está familiarizado con la Herencia en Java y conocimientos básicos de la API de conectividad con bases
de datos JDBC: Java DataBase Connectivity. (1.664 palabras).
Cuando nos sumergimos en el mundo de las aplicaciones
multiusuario en tiempo real, es conveniente implementar una estructura de datos que incremente drásticamente el
rendimiento de nuestro programa. Por ejemplo, suponiendo que el tiempo medio que nuestra aplicación invierte en
transacciones básicas (como construir objetos) sea de unos 250 milisegundos, utilizando un pool de objetos
conseguiremos reducir ese tiempo hasta los 30 milisegundos.
La idea básica de un pool de objetos es muy similar a
cómo se trabaja en una biblioteca: si quieres leer un libro, lo tomas prestado, lo lees y finalmente lo devuelves.
Obviamente, es una operación mucho más económica que comprarlo y eso lo notan nuestros bolsillos. De la misma forma,
para un proceso es más económico (con respecto a memoria y velocidad) tomar prestado un objeto, trabajar con él y
finalmente devolverlo a su lugar de origen. En otras palabras, supongamos que nuestra aplicación necesita un objeto,
y en vez de instanciarlo lo obtiene de una estructura que contiene ya esos objetos construidos: el pool de objetos.
Hay que tener en cuenta que existen unas pequeñas diferencias entre nuestro pool y la biblioteca. Es decir,
si necesitamos un libro en concreto, pero todas las copias están cogidas, tendremos que esperar a que alguien
devuelva una copia para poder obtener la nuestra. Está claro que nuestra aplicación no puede estar esperando
tranquilamente a que otras devuelvan el objeto que necesita, por lo que el pool creará copias nuevas cuando sea necesario.
Para no saturar nuestro pool de objetos no utilizados habrá que buscar alguna forma de poder limpiarlo periódicamente.
El pool implementado en este artículo es lo suficientemente genérico como para gestionar el almacenamiento,
el seguimiento
y los tiempos de expiración de los objetos almacenados. Por el contrario, la construcción, validación y destrucción
de los mismos se conseguirán empleando la Herencia (subclassing). Es decir, si queremos un pool de conexiones a
una base de datos, partiremos del comportamiento base del pool de objetos, heredando atributos y métodos de la
clase que lo define, y añadiendo el resto de la funcionalidad en la clase derivada resultante.
Ahora que tenemos
claros los conceptos básicos, pasemos a la práctica. A continuación puedes apreciar la implementación del pool de
objetos. Parte del código señalado por los puntos suspensivos (...) se verá más adelante.
import java.util.Hashtable;
import java.util.Enumeration;
public abstract class PoolObjetos{
private long tiempoExpiracion;
private Hashtable bloqueados, nobloqueados;
PoolObjetos(){...}
abstract Object crear();
abstract boolean validar( Object o );
abstract void expirar( Object o );
synchronized Object checkOut() {...}
synchronized void checkIn( Object o ){...}
}
El almacenamiento interno de los objetos se gestionará con dos tablas hash, una para los objetos bloqueados
y otra para los no bloqueados. Los mismos objetos serán las claves de las entradas de la tabla hash y la última
vez que se utilizó (en milisegundos), el valor asociado a cada clave. De esta manera, si un objeto supera el tiempo
de expiración, podremos eliminarlo y liberar memoria. A título de curiosidad, el pool de objetos debería permitir a
las clases que hereden de él establecer el tamaño inicial de las tablas hash, su factor de carga y el
tiempo de expiración. Estos parámetros se han preestablecido en el constructor para no complicar más este ejemplo.
PoolObjetos(){
tiempoExpiracion = 30000; // 30 segundos
bloqueados = new Hashtable();
nobloqueados = new Hashtable();
}
Lo primero que hace el método checkOut()
es comprobar si hay objetos en la tabla de bloqueados.
De ser así, itera en la búsqueda de uno de ellos que sea válido. Ese proceso de validación depende de dos cosas.
Primera, el pool comprueba que el tiempo en que se utilizó ese objeto por última vez no supera el tiempo de expiración,
la clase que marca esa última vez es la derivada de nuestra PoolObjetos
.
Segunda, el pool llama el método abstracto validate()
, definido en la subclase, que comprueba alguna propiedad
específica del propio objeto para poder reutilizarlo. Si esta validación falla, se libera el objeto y
continua el bucle al siguiente objeto en la tabla.
Cuando encontramos un objeto válido, lo movemos a la
tabla de bloqueados y lo entregamos al proceso o aplicación que lo ha llamado. Si la tabla de no bloqueados
está vacía, o no contiene objetos válidos, tendremos que instanciar nuevo objeto.
synchronized Object checkOut() {
long ahora = System.currentTimeMillis();
Object o;
if( nobloqueados.size() > 0 ){
Enumeration e = nobloqueados.keys();
while( e.hasMoreElements() ){
o = e.nextElement();
if( ( ahora - ( ( Long ) nobloqueados.get( o ) ).longValue() ) > tiempoExpiracion ){
// el objeto ha expirado
nobloqueados.remove( o );
expirar( o ); o = null;
}
else{
if( validar( o ) ){
nobloqueados.remove( o );
bloqueados.put( o, new Long( ahora ) );
return( o );
}
else{
// no es un objeto válido
nobloqueados.remove( o );
expirar( o );
o = null;
}
}
}
}
// no hay objetos disponibles, por lo que creamos uno nuevo
o = crear();
bloqueados.put( o, new Long( ahora ) );
return( o );
}
Ya pasó lo peor, de aquí en adelante todo se vuelve cuesta abajo. El método checkIn()
simplemente mueve el objeto,
dado por parámetros, desde la tabla de bloqueados ha la de los no bloqueados.
synchronized void checkIn( Object o ){
bloqueados.remove( o );
nobloqueados.put( o, new Long( System.currentTimeMillis() ) );
}
Los tres métodos restantes son abstractos, por lo que tendremos que implementarlos en la subclase.
Una aplicación interesante y frecuente del pool de objetos es la de implementar un pool de conexiones
a una base de datos. A continuación, la clase encargada de todo esto: JDBCConnectionPool
.
import java.sql.*;
public class JDBCConnectionPool extends PoolObjetos {
private String servidor, usuario, contraseña, bd;
public JDBCConnectionPool(String driver,
String servidor,
String usuario,
String contraseña, String bd ){
...
}
Object crear() {...}
boolean validar( Object o ) { ... }
void expirar( Object o ) {...}
public Connection getConnection() { ... }
public void returnConnection( Connection c ) { ... }
}
El pool de conexiones necesitará que nuestra aplicación le especifique el driver de nuestra base de datos, el servidor,
nombre de usuario, contraseña y base de datos a través del constructor. Esto último son conceptos específicos de la API JDBC
(Java DataBase Connectivity).
public JDBCConnectionPool(String driver,
String servidor,
String usuario,
String contraseña,
String bd )
{
try {
Class.forName( driver ).newInstance();
}
catch( Exception e ){
e.printStackTrace();
}
this.servidor = servidor;
this.usuario = usuario;
this.contraseña = contraseña;
this.bd = bd;
}
Ahora procederemos a implementar los métodos abstractos definidos en la clase PoolObjetos:crear()
, expirar()
y validar()
.
Como ya vimos en el checkOut()
, la clase PoolObjetos
invocará el método crear()
desde la subclase, cuando necesite instanciar un nuevo objeto.
Para nuestro pool de conexiones, todo lo que tenemos que hacer es crear un nuevo objeto de tipoConnection
y devolverlo. De nuevo, para no complicar este artículo, simplemente se capturan las posibles
excepciones y no hacemos nada al respecto.
Object crear(){
try {
return(DriverManager.getConnection( jdbc:mysql://" +
servidor + ":3306/" + bd +
"?user="+usuario+";password="+contraseña ) );
}
catch( SQLException e ) {
e.printStackTrace();
return( null );
}
}
Antes de que el pool de objetos libere un objeto expirado o inválido al recolector de basura,
se lo pasa al método expirar()
de la subclase por si hay que hacer algún tipo de limpieza en el último
momento. (esto es muy parecido al método finalize()
del recolector de basura).
En el caso del JDBCConnectionPool
, lo único que tenemos que hacer es cerrar esa conexión.
void expirar( Object o ){
try {
( ( Connection ) o ).close();
}
catch( SQLException e ){
e.printStackTrace();
}
}
Y finalmente, necesitamos implementar el método validar()
que el pool de objetos llama para asegurarse
de que un objeto todavía es válido para usarse. También éste es el lugar par realizar alguna reinicialización .
Para nuestra clase JDBCConnectionPool
simplemente comprobamos que la conexión a la base de datos continúa abierta.
boolean validar( Object o) {
try {
return( ! ( ( Connection ) o ).isClosed() );
}
catch( SQLException e ){
e.printStackTrace();
return( false );
}
}
Con los siguientes métodos, el pool de conexiones permitirá a nuestra aplicación tomar o devolver conexiones.
public Connection getConnection() {
return( ( Connection ) super.checkOut() );
}
public void returnConnection( Connection c ) {
super.checkIn( c );
}
Por último, solo queda comentar que este diseño un par de defectos. Probablemente, el peor sea la posibilidad
de crear un pool de objetos tan grande que habrá elementos que nunca se utilicen, por lo que ya no se aplica
la definición de comportamiento de un pool. Por ejemplo, en el caso de que un montón de procesos requieran un
objeto del pool simultáneamente, se construirán todas las intancias necesarias. Por lo que, si todos los
procesos devuelven los objetos a nuestro pool y el checkOut()
nunca se vuelve a llamar,
no se eliminará ninguno de los objetos. Ésta es una situación poco común, pero puede llegarse a dar con
procesos que tengan un tiempo de espera o idle time. Podemos solventar este problema con un
thread o hilo especializado en la limpieza del pool. Pero el autor se reserva este tema para otro
artículo siguiente, cubriendo también la gestión de errores y la propagación de excepciones para hacer un pool más robusto.
Podeis descargar el código de ejemplo de este artículo de aquí
Sobre el Autor: Thomas E. Davis es un Programador Certificado por Sun.
Actualmente vive en el sur de Florida, pero debido al exceso de trabajo no puede salir mucho de casa.
|
Reader Comments