Crea il tuo ObjectPool in Java, parte 1

L'idea del raggruppamento di oggetti è simile al funzionamento della tua biblioteca locale: quando vuoi leggere un libro, sai che è più economico prendere in prestito una copia dalla biblioteca piuttosto che acquistare la tua copia. Allo stesso modo, è più economico (in relazione alla memoria e alla velocità) per un processo prendere in prestito un oggetto piuttosto che crearne una propria copia. In altre parole, i libri nella biblioteca rappresentano oggetti e gli utenti della biblioteca rappresentano i processi. Quando un processo necessita di un oggetto, ne estrae una copia da un pool di oggetti invece di istanziarne uno nuovo. Il processo restituisce quindi l'oggetto al pool quando non è più necessario.

Ci sono, tuttavia, alcune piccole distinzioni tra il pool di oggetti e l'analogia con la libreria che dovrebbero essere comprese. Se un utente della biblioteca desidera un determinato libro, ma tutte le copie di quel libro vengono ritirate, l'utente deve attendere fino a quando una copia viene restituita. Non vogliamo mai che un processo debba attendere un oggetto, quindi il pool di oggetti creerà nuove copie come necessario. Ciò potrebbe portare a una quantità esorbitante di oggetti in giro nella piscina, quindi terrà anche un conteggio degli oggetti inutilizzati e li pulirà periodicamente.

La progettazione del mio pool di oggetti è abbastanza generica da gestire i tempi di archiviazione, tracciamento e scadenza, ma l'istanza, la convalida e la distruzione di tipi di oggetti specifici devono essere gestite mediante sottoclassi.

Ora che le basi sono state tolte, passiamo al codice. Questo è l'oggetto scheletrico:

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

La memoria interna degli oggetti in pool verrà gestita con due Hashtableoggetti, uno per gli oggetti bloccati e l'altro per gli oggetti sbloccati. Gli oggetti stessi saranno le chiavi della tabella hash e il loro tempo di ultimo utilizzo (in millisecondi di epoca) sarà il valore. Memorizzando l'ultima volta che un oggetto è stato utilizzato, il pool può farlo scadere e liberare memoria dopo un determinato periodo di inattività.

In definitiva, il pool di oggetti consentirebbe alla sottoclasse di specificare la dimensione iniziale delle tabelle hash insieme al loro tasso di crescita e al tempo di scadenza, ma sto cercando di mantenerlo semplice per gli scopi di questo articolo codificando questi valori nel costruttore.

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

Il checkOut()metodo verifica innanzitutto se sono presenti oggetti nella tabella hash sbloccata. In tal caso, li passa in rassegna e cerca uno valido. La convalida dipende da due cose. Innanzitutto, il pool di oggetti verifica che l'ora dell'ultimo utilizzo dell'oggetto non superi l'ora di scadenza specificata dalla sottoclasse. In secondo luogo, il pool di oggetti chiama il validate()metodo astratto , che esegue qualsiasi verifica o reinizializzazione specifica della classe necessaria per riutilizzare l'oggetto. Se l'oggetto non supera la convalida, viene liberato e il ciclo continua con l'oggetto successivo nella tabella hash. Quando viene trovato un oggetto che supera la convalida, viene spostato nella tabella hash bloccata e restituito al processo che lo ha richiesto. Se la tabella hash sbloccata è vuota o nessuno dei suoi oggetti supera la convalida, viene istanziato e restituito un nuovo oggetto.

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

Questo è il metodo più complesso della ObjectPoolclasse, da qui è tutto in discesa. Il checkIn()metodo sposta semplicemente l'oggetto passato dalla tabella hash bloccata alla tabella hash sbloccata.

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

I tre metodi rimanenti sono astratti e pertanto devono essere implementati dalla sottoclasse. Per il bene di questo articolo, creerò un pool di connessioni di database chiamato JDBCConnectionPool. Ecco lo scheletro:

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

Il JDBCConnectionPoolrichiederà l'applicazione per specificare il driver di database, DSN, il nome utente e la password su di istanze (tramite il costruttore). (Se questo è tutto in greco per te, non preoccuparti, JDBC è un altro argomento. Abbi pazienza finché non torneremo al raggruppamento.)

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

Ora possiamo immergerci nell'implementazione dei metodi astratti. Come hai visto nel checkOut()metodo, ObjectPoolchiamerà create () dalla sua sottoclasse quando avrà bisogno di istanziare un nuovo oggetto. Per JDBCConnectionPooltutto quello che dobbiamo fare è creare un nuovo Connectionoggetto e passare di nuovo. Ancora una volta, per mantenere questo articolo semplice, sto gettando al vento cautela e ignorando qualsiasi eccezione e condizione di puntatore nullo.

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

Prima di ObjectPoolliberare un oggetto scaduto (o non valido) per la garbage collection, lo passa al suo expire()metodo sottoclasse per qualsiasi necessaria pulizia dell'ultimo minuto (molto simile al finalize()metodo chiamato dal garbage collector). Nel caso di JDBCConnectionPool, tutto ciò che dobbiamo fare è chiudere la connessione.

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

Infine, dobbiamo implementare il metodo validate () che ObjectPoolchiama per assicurarci che un oggetto sia ancora valido per l'uso. Questo è anche il luogo in cui dovrebbe avvenire qualsiasi reinizializzazione. Perché JDBCConnectionPool, controlliamo solo che la connessione sia ancora aperta.

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

Questo è tutto per la funzionalità interna. JDBCConnectionPoolconsentirà all'applicazione di prendere in prestito e restituire connessioni al database tramite questi metodi incredibilmente semplici e con nomi appropriati.

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

Questo design ha un paio di difetti. Forse la più grande è la possibilità di creare un ampio pool di oggetti che non vengono mai rilasciati. Ad esempio, se un gruppo di processi richiede un oggetto dal pool contemporaneamente, il pool creerà tutte le istanze necessarie. Quindi, se tutti i processi restituiscono gli oggetti al pool, ma checkOut()non vengono più chiamati, nessuno degli oggetti viene ripulito. Questo è un evento raro per le applicazioni attive, ma alcuni processi back-end che hanno un tempo di "inattività" potrebbero produrre questo scenario. Ho risolto questo problema di progettazione con un thread di "pulizia", ​​ma salverò la discussione per la seconda metà di questo articolo. Tratterò anche la corretta gestione degli errori e la propagazione delle eccezioni per rendere il pool più robusto per le applicazioni mission-critical.

Thomas E. Davis è un programmatore Java certificato Sun. Attualmente risiede nel soleggiato sud della Florida, ma soffre di maniaco del lavoro e trascorre la maggior parte del suo tempo in casa.

Questa storia, "Crea il tuo ObjectPool in Java, parte 1" è stata originariamente pubblicata da JavaWorld.