Risorse del pool utilizzando Commons Pool Framework di Apache

Il pool di risorse (chiamato anche pool di oggetti) tra più client è una tecnica utilizzata per promuovere il riutilizzo degli oggetti e per ridurre il sovraccarico della creazione di nuove risorse, con conseguente miglioramento delle prestazioni e della velocità effettiva. Immagina un'applicazione server Java per impieghi gravosi che invia centinaia di query SQL aprendo e chiudendo connessioni per ogni richiesta SQL. O un server Web che serve centinaia di richieste HTTP, gestendo ogni richiesta generando un thread separato. Oppure immagina di creare un'istanza del parser XML per ogni richiesta per analizzare un documento senza riutilizzare le istanze. Questi sono alcuni degli scenari che garantiscono l'ottimizzazione delle risorse utilizzate.

L'utilizzo delle risorse potrebbe rivelarsi critico a volte per le applicazioni pesanti. Alcuni siti Web famosi sono stati chiusi a causa della loro incapacità di gestire carichi pesanti. La maggior parte dei problemi relativi a carichi pesanti possono essere gestiti, a livello macro, utilizzando funzionalità di clustering e bilanciamento del carico. Rimangono preoccupazioni a livello di applicazione per quanto riguarda la creazione eccessiva di oggetti e la disponibilità di risorse server limitate come memoria, CPU, thread e connessioni al database, che potrebbero rappresentare potenziali colli di bottiglia e, se non utilizzate in modo ottimale, far crollare l'intero server.

In alcune situazioni, la politica di utilizzo del database potrebbe imporre un limite al numero di connessioni simultanee. Inoltre, un'applicazione esterna potrebbe dettare o limitare il numero di connessioni aperte simultanee. Un tipico esempio è un registro di dominio (come Verisign) che limita il numero di connessioni socket attive disponibili per i registrar (come BulkRegister). La condivisione delle risorse si è dimostrata una delle migliori opzioni per gestire questi tipi di problemi e, in una certa misura, aiuta anche a mantenere i livelli di servizio richiesti per le applicazioni aziendali.

La maggior parte dei fornitori di server di applicazioni J2EE fornisce pool di risorse come parte integrante dei loro contenitori Web ed EJB (Enterprise JavaBean). Per le connessioni al database, il fornitore del server di solito fornisce un'implementazione DataSourcedell'interfaccia, che funziona insieme ConnectionPoolDataSourceall'implementazione del fornitore del driver JDBC (Java Database Connectivity) . L' ConnectionPoolDataSourceimplementazione funge da factory di connessione del gestore risorse per gli java.sql.Connectionoggetti in pool . Allo stesso modo, le istanze EJB di bean di sessione senza stato, bean di messaggi e bean di entità vengono raggruppate in contenitori EJB per una maggiore velocità effettiva e prestazioni. Anche le istanze del parser XML sono candidate per il pool, poiché la creazione di istanze del parser consuma gran parte delle risorse di un sistema.

Un'implementazione di successo del pool di risorse open source è DBCP del framework Commons Pool, un componente di pool di connessioni di database della Apace Software Foundation che è ampiamente utilizzato nelle applicazioni aziendali di classe di produzione. In questo articolo, discuto brevemente gli interni del framework Commons Pool e poi lo uso per implementare un pool di thread.

Diamo prima un'occhiata a ciò che offre il framework.

Framework di Commons Pool

Il framework Commons Pool offre un'implementazione di base e robusta per il pool di oggetti arbitrari. Sono fornite diverse implementazioni, ma per gli scopi di questo articolo utilizziamo l'implementazione più generica, il GenericObjectPool. Utilizza a CursorableLinkedList, che è un'implementazione di elenchi a doppio collegamento (parte delle collezioni Jakarta Commons), come struttura dati sottostante per contenere gli oggetti raggruppati.

Inoltre, il framework fornisce una serie di interfacce che forniscono metodi del ciclo di vita e metodi di supporto per la gestione, il monitoraggio e l'estensione del pool.

L'interfaccia org.apache.commons.PoolableObjectFactorydefinisce i seguenti metodi del ciclo di vita, che si dimostrano essenziali per l'implementazione di un componente di pooling:

 // Creates an instance that can be returned by the pool public Object makeObject() {} // Destroys an instance no longer needed by the pool public void destroyObject(Object obj) {} // Validate the object before using it public boolean validateObject(Object obj) {} // Initialize an instance to be returned by the pool public void activateObject(Object obj) {} // Uninitialize an instance to be returned to the pool public void passivateObject(Object obj) {}

Come puoi capire dalle firme del metodo, questa interfaccia si occupa principalmente di quanto segue:

  • makeObject(): Implementa la creazione dell'oggetto
  • destroyObject(): Implementa la distruzione dell'oggetto
  • validateObject(): Convalida l'oggetto prima che venga utilizzato
  • activateObject(): Implementa il codice di inizializzazione dell'oggetto
  • passivateObject(): Implementa il codice di annullamento dell'inizializzazione dell'oggetto

Un'altra interfaccia principale: org.apache.commons.ObjectPooldefinisce i seguenti metodi per la gestione e il monitoraggio del pool:

 // Obtain an instance from my pool Object borrowObject() throws Exception; // Return an instance to my pool void returnObject(Object obj) throws Exception; // Invalidates an object from the pool void invalidateObject(Object obj) throws Exception; // Used for pre-loading a pool with idle objects void addObject() throws Exception; // Return the number of idle instances int getNumIdle() throws UnsupportedOperationException; // Return the number of active instances int getNumActive() throws UnsupportedOperationException; // Clears the idle objects void clear() throws Exception, UnsupportedOperationException; // Close the pool void close() throws Exception; //Set the ObjectFactory to be used for creating instances void setFactory(PoolableObjectFactory factory) throws IllegalStateException, UnsupportedOperationException;

L' ObjectPoolimplementazione dell'interfaccia prende PoolableObjectFactorycome argomento nei suoi costruttori, delegando così la creazione di oggetti alle sue sottoclassi. Non parlo molto di design pattern qui poiché non è questo il nostro obiettivo. Per i lettori interessati a guardare i diagrammi delle classi UML, vedere Risorse.

Come accennato in precedenza, la classe org.apache.commons.GenericObjectPoolè solo un'implementazione org.apache.commons.ObjectPooldell'interfaccia. Il framework fornisce anche implementazioni per pool di oggetti con chiave, utilizzando le interfacce org.apache.commons.KeyedObjectPoolFactorye org.apache.commons.KeyedObjectPool, dove è possibile associare un pool a una chiave (come in HashMap) e quindi gestire più pool.

La chiave per una strategia di pooling di successo dipende da come configuriamo il pool. I pool configurati male possono essere divoratori di risorse, se i parametri di configurazione non sono ben regolati. Diamo un'occhiata ad alcuni parametri importanti e al loro scopo.

Dettagli di configurazione

Il pool può essere configurato utilizzando la GenericObjectPool.Configclasse, che è una classe interna statica. In alternativa, potremmo semplicemente usare GenericObjectPooli metodi setter di per impostare i valori.

Il seguente elenco descrive in dettaglio alcuni dei parametri di configurazione disponibili per l' GenericObjectPoolimplementazione:

  • maxIdle: Il numero massimo di istanze inattive nel pool, senza che vengano rilasciati oggetti aggiuntivi.
  • minIdle: Il numero minimo di istanze inattive nel pool, senza che vengano creati oggetti aggiuntivi.
  • maxActive: Il numero massimo di istanze attive nel pool.
  • timeBetweenEvictionRunsMillis: Il numero di millisecondi di sospensione tra le esecuzioni del thread del programma di eliminazione degli oggetti inattivi. Se negativo, non verrà eseguito alcun thread del programma di eliminazione degli oggetti inattivi. Utilizzare questo parametro solo quando si desidera eseguire il thread del programma di eliminazione.
  • minEvictableIdleTimeMillis: La quantità minima di tempo in cui un oggetto, se attivo, può rimanere inattivo nel pool prima che sia idoneo per lo sfratto da parte del programma di eliminazione degli oggetti inattivi. Se viene fornito un valore negativo, nessun oggetto viene rimosso a causa del solo tempo di inattività.
  • testOnBorrow: Quando "true", gli oggetti vengono convalidati. Se l'oggetto non supera la convalida, verrà eliminato dal pool e il pool tenterà di prenderne in prestito un altro.

È necessario fornire valori ottimali per i parametri di cui sopra per ottenere le massime prestazioni e produttività. Poiché il modello di utilizzo varia da applicazione a applicazione, ottimizzare il pool con diverse combinazioni di parametri per arrivare alla soluzione ottimale.

Per capire di più sul pool e sui suoi interni, implementiamo un pool di thread.

Requisiti del pool di thread proposti

Supponiamo che ci sia stato detto di progettare e implementare un componente del pool di thread per un pianificatore di lavori per attivare i lavori a pianificazioni specificate e segnalare il completamento e, possibilmente, il risultato dell'esecuzione. In tale scenario, l'obiettivo del nostro pool di thread è quello di raggruppare un numero prerequisito di thread ed eseguire i lavori pianificati in thread indipendenti. I requisiti sono riassunti come segue:

  • Il thread dovrebbe essere in grado di invocare qualsiasi metodo di classe arbitrario (il lavoro pianificato)
  • Il thread dovrebbe essere in grado di restituire il risultato di un'esecuzione
  • Il thread dovrebbe essere in grado di segnalare il completamento di un'attività

Il primo requisito fornisce la possibilità di un'implementazione debolmente accoppiata poiché non ci obbliga a implementare un'interfaccia come Runnable. Inoltre semplifica l'integrazione. Possiamo implementare il nostro primo requisito fornendo al thread le seguenti informazioni:

  • Il nome della classe
  • Il nome del metodo da richiamare
  • I parametri da passare al metodo
  • I tipi di parametro dei parametri passati

Il secondo requisito consente a un client che utilizza il thread di ricevere il risultato dell'esecuzione. Una semplice implementazione sarebbe memorizzare il risultato dell'esecuzione e fornire un metodo di accesso come getResult().

Il terzo requisito è in qualche modo correlato al secondo requisito. Segnalare il completamento di un'attività può anche significare che il client è in attesa di ottenere il risultato dell'esecuzione. Per gestire questa capacità, possiamo fornire una qualche forma di meccanismo di callback. Il meccanismo di callback più semplice può essere implementato utilizzando le java.lang.Objects' wait()e notify()semantica. In alternativa, potremmo usare il pattern Observer , ma per ora manteniamo le cose semplici. Potresti essere tentato di utilizzare il metodo java.lang.Threaddella classe join(), ma ciò non funzionerà poiché il thread in pool non completa mai il suo run()metodo e continua a funzionare finché il pool ne ha bisogno.

Ora che abbiamo i nostri requisiti pronti e un'idea approssimativa su come implementare il pool di thread, è il momento di fare un po 'di codice reale.

In questa fase, il nostro diagramma delle classi UML del progetto proposto è simile alla figura seguente.

Implementazione del pool di thread

L'oggetto thread che metteremo in comune è in realtà un wrapper attorno all'oggetto thread. Chiamiamo il wrapper la WorkerThreadclasse, che estende la java.lang.Threadclasse. Prima di poter iniziare a scrivere codice WorkerThread, dobbiamo implementare i requisiti del framework. Come abbiamo visto prima, dobbiamo implementare PoolableObjectFactory, che funge da factory, per creare i nostri poolable WorkerThread. Una volta che la fabbrica è pronta, implementiamo l' ThreadPoolestensione estendendo il file GenericObjectPool. Quindi, finiamo il nostro WorkerThread.

Implementazione dell'interfaccia PoolableObjectFactory

Iniziamo con l' PoolableObjectFactoryinterfaccia e cerchiamo di implementare i metodi del ciclo di vita necessari per il nostro pool di thread. Scriviamo la classe di fabbrica ThreadObjectFactorycome segue:

public class ThreadObjectFactory implements PoolableObjectFactory{

public Object makeObject() { return new WorkerThread(); } public void destroyObject(Object obj) { if (obj instanceof WorkerThread) { WorkerThread rt = (WorkerThread) obj; rt.setStopped(true);//Make the running thread stop } } public boolean validateObject(Object obj) { if (obj instanceof WorkerThread) { WorkerThread rt = (WorkerThread) obj; if (rt.isRunning()) { if (rt.getThreadGroup() == null) { return false; } return true; } } return true; } public void activateObject(Object obj) { log.debug(" activateObject..."); }

public void passivateObject(Object obj) { log.debug(" passivateObject..." + obj); if (obj instanceof WorkerThread) { WorkerThread wt = (WorkerThread) obj; wt.setResult(null); //Clean up the result of the execution } } }

Esaminiamo ogni metodo in dettaglio:

Il metodo makeObject()crea l' WorkerThreadoggetto. Per ogni richiesta, il pool viene controllato per vedere se deve essere creato un nuovo oggetto o se deve essere riutilizzato un oggetto esistente. Ad esempio, se una particolare richiesta è la prima richiesta e il pool è vuoto, l' ObjectPoolimplementazione chiama makeObject()e aggiunge WorkerThreadal pool.

Il metodo destroyObject()rimuove l' WorkerThreadoggetto dal pool impostando un flag booleano e quindi arrestando il thread in esecuzione. Guarderemo di nuovo questo pezzo più tardi, ma noteremo che ora stiamo assumendo il controllo su come i nostri oggetti vengono distrutti.