Construya su propio ObjectPool en Java, Parte 1

La idea de la agrupación de objetos es similar a la operación de su biblioteca local: cuando quiere leer un libro, sabe que es más barato pedir prestada una copia de la biblioteca que comprar su propia copia. Asimismo, es más barato (en relación con la memoria y la velocidad) que un proceso tome prestado un objeto en lugar de crear su propia copia. En otras palabras, los libros de la biblioteca representan objetos y los usuarios de la biblioteca representan los procesos. Cuando un proceso necesita un objeto, extrae una copia de un grupo de objetos en lugar de crear una instancia de uno nuevo. Luego, el proceso devuelve el objeto al grupo cuando ya no es necesario.

Sin embargo, hay algunas distinciones menores entre la agrupación de objetos y la analogía de la biblioteca que deben entenderse. Si un usuario de la biblioteca quiere un libro en particular, pero se han prestado todas las copias de ese libro, el usuario debe esperar hasta que se le devuelva una copia. No queremos que un proceso tenga que esperar nunca un objeto, por lo que el grupo de objetos creará nuevas copias según sea necesario. Esto podría llevar a una cantidad exorbitante de objetos tirados en la piscina, por lo que también llevará un registro de los objetos no utilizados y los limpiará periódicamente.

El diseño de mi grupo de objetos es lo suficientemente genérico para manejar el almacenamiento, el seguimiento y los tiempos de vencimiento, pero la creación de instancias, la validación y la destrucción de tipos de objetos específicos deben manejarse mediante subclases.

Ahora que lo básico fuera del camino, saltemos al código. Este es el objeto esquelético:

 public abstract class ObjectPool { private long expirationTime; private Hashtable locked, unlocked; abstract Object create(); abstract boolean validate( Object o ); abstract void expire( Object o ); synchronized Object checkOut(){...} synchronized void checkIn( Object o ){...} } 

El almacenamiento interno de los objetos agrupados se manejará con dos Hashtableobjetos, uno para los objetos bloqueados y el otro para los desbloqueados. Los objetos mismos serán las claves de la tabla hash y su último tiempo de uso (en milisegundos de época) será el valor. Al almacenar la última vez que se utilizó un objeto, el grupo puede caducarlo y liberar memoria después de un período de inactividad específico.

En última instancia, el grupo de objetos permitiría a la subclase especificar el tamaño inicial de las tablas hash junto con su tasa de crecimiento y el tiempo de vencimiento, pero estoy tratando de mantenerlo simple para los propósitos de este artículo codificando estos valores en el constructor.

 ObjectPool() { expirationTime = 30000; // 30 seconds locked = new Hashtable(); unlocked = new Hashtable(); } 

El checkOut()método primero verifica si hay algún objeto en la tabla hash desbloqueada. Si es así, los recorre y busca uno válido. La validación depende de dos cosas. Primero, el grupo de objetos verifica que el tiempo de último uso del objeto no exceda el tiempo de vencimiento especificado por la subclase. En segundo lugar, el grupo de objetos llama al validate()método abstracto , que realiza cualquier verificación o reinicialización específica de la clase que sea necesaria para reutilizar el objeto. Si el objeto falla en la validación, se libera y el bucle continúa con el siguiente objeto de la tabla hash. Cuando se encuentra un objeto que pasa la validación, se mueve a la tabla hash bloqueada y se devuelve al proceso que lo solicitó. Si la tabla hash desbloqueada está vacía, o ninguno de sus objetos pasa la validación, se crea una instancia y se devuelve un nuevo objeto.

 synchronized Object checkOut() { long now = System.currentTimeMillis(); Object o; if( unlocked.size() > 0 ) { Enumeration e = unlocked.keys(); while( e.hasMoreElements() ) { o = e.nextElement(); if( ( now - ( ( Long ) unlocked.get( o ) ).longValue() ) > expirationTime ) { // object has expired unlocked.remove( o ); expire( o ); o = null; } else { if( validate( o ) ) { unlocked.remove( o ); locked.put( o, new Long( now ) ); return( o ); } else { // object failed validation unlocked.remove( o ); expire( o ); o = null; } } } } // no objects available, create a new one o = create(); locked.put( o, new Long( now ) ); return( o ); } 

Ese es el método más complejo de la ObjectPoolclase, todo es cuesta abajo desde aquí. El checkIn()método simplemente mueve el objeto pasado de la tabla hash bloqueada a la tabla hash desbloqueada.

synchronized void checkIn( Object o ) { locked.remove( o ); unlocked.put( o, new Long( System.currentTimeMillis() ) ); } 

Los tres métodos restantes son abstractos y, por lo tanto, deben ser implementados por la subclase. Por el bien de este artículo, voy a crear un grupo de conexiones de base de datos llamado JDBCConnectionPool. Aquí está el esqueleto:

 public class JDBCConnectionPool extends ObjectPool { private String dsn, usr, pwd; public JDBCConnectionPool(){...} create(){...} validate(){...} expire(){...} public Connection borrowConnection(){...} public void returnConnection(){...} } 

El JDBCConnectionPoolrequerirá que la aplicación especifique el controlador de la base de datos, DSN, nombre de usuario y contraseña en la instanciación (a través del constructor). (Si todo esto es griego para usted, no se preocupe, JDBC es otro tema. Solo tengan paciencia conmigo hasta que volvamos a la agrupación).

 public JDBCConnectionPool( String driver, String dsn, String usr, String pwd ) { try { Class.forName( driver ).newInstance(); } catch( Exception e ) { e.printStackTrace(); } this.dsn = dsn; this.usr = usr; this.pwd = pwd; } 

Ahora podemos sumergirnos en la implementación de los métodos abstractos. Como vio en el checkOut()método, ObjectPoolllamará a create () desde su subclase cuando necesite crear una instancia de un nuevo objeto. Porque JDBCConnectionPool, todo lo que tenemos que hacer es crear un nuevo Connectionobjeto y devolverlo. Nuevamente, por el bien de mantener este artículo simple, estoy tirando la precaución al viento e ignorando cualquier excepción y condición de puntero nulo.

 Object create() { try { return( DriverManager.getConnection( dsn, usr, pwd ) ); } catch( SQLException e ) { e.printStackTrace(); return( null ); } } 

Antes de que ObjectPoollibere un objeto caducado (o no válido) para la recolección de basura, lo pasa a su expire()método subclasificado para cualquier limpieza de último minuto necesaria (muy similar al finalize()método llamado por el recolector de basura). En el caso de JDBCConnectionPool, todo lo que tenemos que hacer es cerrar la conexión.

void expire( Object o ) { try { ( ( Connection ) o ).close(); } catch( SQLException e ) { e.printStackTrace(); } } 

Y finalmente, necesitamos implementar el método validate () que ObjectPoolllama para asegurarnos de que un objeto sigue siendo válido para su uso. Este es también el lugar donde debe tener lugar cualquier reinicialización. Porque JDBCConnectionPool, solo verificamos que la conexión todavía esté abierta.

 boolean validate( Object o ) { try { return( ! ( ( Connection ) o ).isClosed() ); } catch( SQLException e ) { e.printStackTrace(); return( false ); } } 

Eso es todo por la funcionalidad interna. JDBCConnectionPoolpermitirá que la aplicación tome prestadas y devuelva conexiones de base de datos a través de estos métodos increíblemente simples y con nombres adecuados.

 public Connection borrowConnection() { return( ( Connection ) super.checkOut() ); } public void returnConnection( Connection c ) { super.checkIn( c ); } 

Este diseño tiene un par de defectos. Quizás la más grande es la posibilidad de crear una gran cantidad de objetos que nunca se liberan. Por ejemplo, si un grupo de procesos solicita un objeto del grupo simultáneamente, el grupo creará todas las instancias necesarias. Luego, si todos los procesos devuelven los objetos al grupo, pero checkOut()nunca se vuelven a llamar, ninguno de los objetos se limpia. Esta es una ocurrencia rara para aplicaciones activas, pero algunos procesos de back-end que tienen tiempo "inactivo" pueden producir este escenario. Resolví este problema de diseño con un hilo de "limpieza", pero guardaré esa discusión para la segunda mitad de este artículo. También cubriré el manejo adecuado de errores y la propagación de excepciones para hacer que el grupo sea más robusto para aplicaciones de misión crítica.

Thomas E. Davis es un programador Java certificado por Sun. Actualmente reside en el soleado sur de Florida, pero sufre como adicto al trabajo y pasa la mayor parte del tiempo en interiores.

Esta historia, "Construya su propio ObjectPool en Java, Parte 1" fue publicada originalmente por JavaWorld.