Poolressourcen mit dem Commons Pool Framework von Apache

Das Zusammenlegen von Ressourcen (auch als Objekt-Pooling bezeichnet) zwischen mehreren Clients ist eine Technik, mit der die Wiederverwendung von Objekten gefördert und der Aufwand für das Erstellen neuer Ressourcen verringert wird, was zu einer besseren Leistung und einem besseren Durchsatz führt. Stellen Sie sich eine leistungsstarke Java-Serveranwendung vor, die Hunderte von SQL-Abfragen sendet, indem sie für jede SQL-Anforderung Verbindungen öffnet und schließt. Oder ein Webserver, der Hunderte von HTTP-Anforderungen bedient und jede Anforderung verarbeitet, indem er einen separaten Thread erzeugt. Oder stellen Sie sich vor, Sie erstellen eine XML-Parser-Instanz für jede Anforderung zum Parsen eines Dokuments, ohne die Instanzen wiederzuverwenden. Dies sind einige der Szenarien, die eine Optimierung der verwendeten Ressourcen rechtfertigen.

Die Ressourcennutzung kann sich für Hochleistungsanwendungen manchmal als kritisch erweisen. Einige berühmte Websites wurden aufgrund ihrer Unfähigkeit, mit schweren Lasten umzugehen, geschlossen. Die meisten Probleme im Zusammenhang mit schweren Lasten können auf Makroebene mithilfe von Clustering- und Lastausgleichsfunktionen behoben werden. Auf Anwendungsebene bestehen weiterhin Bedenken hinsichtlich einer übermäßigen Objekterstellung und der Verfügbarkeit begrenzter Serverressourcen wie Speicher, CPU, Threads und Datenbankverbindungen, die potenzielle Engpässe darstellen und, wenn sie nicht optimal genutzt werden, den gesamten Server zum Erliegen bringen können.

In einigen Situationen kann die Datenbanknutzungsrichtlinie eine Begrenzung der Anzahl gleichzeitiger Verbindungen erzwingen. Eine externe Anwendung kann auch die Anzahl der gleichzeitig geöffneten Verbindungen vorschreiben oder einschränken. Ein typisches Beispiel ist eine Domänenregistrierung (wie Verisign), die die Anzahl der verfügbaren aktiven Socket-Verbindungen für Registrare (wie BulkRegister) begrenzt. Das Bündeln von Ressourcen hat sich als eine der besten Optionen für die Behandlung dieser Art von Problemen erwiesen und trägt in gewissem Umfang auch zur Aufrechterhaltung der erforderlichen Service-Levels für Unternehmensanwendungen bei.

Die meisten Anbieter von J2EE-Anwendungsservern bieten Ressourcenpooling als integralen Bestandteil ihrer Web- und EJB-Container (Enterprise JavaBean) an. Für Datenbankverbindungen stellt der Serverhersteller normalerweise eine Implementierung der DataSourceSchnittstelle bereit, die in Verbindung mit der Implementierung des JDBC-Treiberanbieters (Java Database Connectivity) funktioniert ConnectionPoolDataSource. Die ConnectionPoolDataSourceImplementierung dient als Verbindungsmanager für Ressourcenmanager für gepoolte java.sql.ConnectionObjekte. In ähnlicher Weise werden EJB-Instanzen von zustandslosen Session-Beans, nachrichtengesteuerten Beans und Entity-Beans in EJB-Containern zusammengefasst, um einen höheren Durchsatz und eine höhere Leistung zu erzielen. XML-Parser-Instanzen sind auch Kandidaten für das Pooling, da die Erstellung von Parser-Instanzen einen Großteil der Ressourcen eines Systems beansprucht.

Eine erfolgreiche Open-Source-Implementierung für das Ressourcen-Pooling ist das DBCP des Commons Pool-Frameworks, eine Datenbankverbindungs-Pooling-Komponente der Apace Software Foundation, die in Unternehmensanwendungen der Produktionsklasse häufig verwendet wird. In diesem Artikel werde ich kurz auf die Interna des Commons Pool-Frameworks eingehen und es dann zum Implementieren eines Thread-Pools verwenden.

Schauen wir uns zunächst an, was das Framework bietet.

Commons Pool Framework

Das Commons Pool Framework bietet eine grundlegende und robuste Implementierung zum Poolen beliebiger Objekte. Es werden mehrere Implementierungen bereitgestellt, aber für die Zwecke dieses Artikels verwenden wir die allgemeinste Implementierung, die GenericObjectPool. Es verwendet eine CursorableLinkedListImplementierung mit doppelt verknüpften Listen (Teil der Jakarta Commons-Sammlungen) als zugrunde liegende Datenstruktur für die Speicherung der Objekte, die gepoolt werden.

Darüber hinaus bietet das Framework eine Reihe von Schnittstellen, die Lebenszyklusmethoden und Hilfsmethoden zum Verwalten, Überwachen und Erweitern des Pools bereitstellen.

Die Schnittstelle org.apache.commons.PoolableObjectFactorydefiniert die folgenden Lebenszyklusmethoden, die sich für die Implementierung einer Pooling-Komponente als wesentlich erweisen:

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

Wie Sie an den Methodensignaturen erkennen können, befasst sich diese Schnittstelle hauptsächlich mit Folgendem:

  • makeObject(): Implementieren Sie die Objekterstellung
  • destroyObject(): Implementieren Sie die Objektzerstörung
  • validateObject(): Überprüfen Sie das Objekt, bevor es verwendet wird
  • activateObject(): Implementieren Sie den Objektinitialisierungscode
  • passivateObject(): Implementieren Sie den Objekt-Uninitialisierungscode

Eine weitere org.apache.commons.ObjectPoolKernschnittstelle - definiert die folgenden Methoden zum Verwalten und Überwachen des Pools:

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

Die ObjectPoolImplementierung der Schnittstelle verwendet PoolableObjectFactoryein Argument in ihren Konstruktoren und delegiert dadurch die Objekterstellung an ihre Unterklassen. Ich spreche hier nicht viel über Designmuster, da dies nicht unser Fokus ist. Für Leser, die sich die UML-Klassendiagramme ansehen möchten, siehe Ressourcen.

Wie oben erwähnt, ist die Klasse org.apache.commons.GenericObjectPoolnur eine Implementierung der org.apache.commons.ObjectPoolSchnittstelle. Das Framework bietet auch Implementierungen für Pools mit Schlüsselobjekten unter Verwendung der Schnittstellen org.apache.commons.KeyedObjectPoolFactoryund org.apache.commons.KeyedObjectPool, bei denen ein Pool einem Schlüssel zugeordnet werden kann (wie in HashMap) und somit mehrere Pools verwaltet werden können.

Der Schlüssel zu einer erfolgreichen Pooling-Strategie hängt davon ab, wie wir den Pool konfigurieren. Schlecht konfigurierte Pools können Ressourcenfresser sein, wenn die Konfigurationsparameter nicht gut abgestimmt sind. Schauen wir uns einige wichtige Parameter und ihren Zweck an.

Konfigurationsdetails

Der Pool kann mithilfe der GenericObjectPool.ConfigKlasse konfiguriert werden , bei der es sich um eine statische innere Klasse handelt. Alternativ können wir auch die GenericObjectPoolSetter-Methoden von 'verwenden, um die Werte festzulegen.

In der folgenden Liste sind einige der verfügbaren Konfigurationsparameter für die GenericObjectPoolImplementierung aufgeführt:

  • maxIdle: Die maximale Anzahl schlafender Instanzen im Pool, ohne dass zusätzliche Objekte freigegeben werden.
  • minIdle: Die Mindestanzahl schlafender Instanzen im Pool, ohne dass zusätzliche Objekte erstellt werden.
  • maxActive: Die maximale Anzahl aktiver Instanzen im Pool.
  • timeBetweenEvictionRunsMillis: Die Anzahl der Millisekunden, die zwischen den Durchläufen des Leerlaufobjekt-Evictor-Threads in den Ruhezustand versetzt werden sollen. Wenn dies negativ ist, wird kein Evictor-Thread für inaktive Objekte ausgeführt. Verwenden Sie diesen Parameter nur, wenn der Evictor-Thread ausgeführt werden soll.
  • minEvictableIdleTimeMillis: Die Mindestzeit, die ein Objekt, wenn es aktiv ist, im Pool im Leerlauf sitzen kann, bevor es vom Räumungsobjekt-Räumer entfernt werden kann. Wenn ein negativer Wert angegeben wird, werden allein aufgrund der Leerlaufzeit keine Objekte entfernt.
  • testOnBorrow: Wenn "true", werden Objekte validiert. Wenn das Objekt die Validierung nicht besteht, wird es aus dem Pool entfernt und der Pool versucht, ein anderes auszuleihen.

Für die oben genannten Parameter sollten optimale Werte angegeben werden, um maximale Leistung und maximalen Durchsatz zu erzielen. Da das Verwendungsmuster von Anwendung zu Anwendung unterschiedlich ist, optimieren Sie den Pool mit verschiedenen Parameterkombinationen, um die optimale Lösung zu erhalten.

Um mehr über den Pool und seine Interna zu erfahren, implementieren wir einen Thread-Pool.

Vorgeschlagene Anforderungen an den Thread-Pool

Angenommen, wir sollten eine Thread-Pool-Komponente für einen Job-Scheduler entwerfen und implementieren, um Jobs nach festgelegten Zeitplänen auszulösen und den Abschluss und möglicherweise das Ergebnis der Ausführung zu melden. In einem solchen Szenario besteht das Ziel unseres Thread-Pools darin, eine vorausgesetzte Anzahl von Threads zu bündeln und die geplanten Jobs in unabhängigen Threads auszuführen. Die Anforderungen sind wie folgt zusammengefasst:

  • Der Thread sollte in der Lage sein, eine beliebige Klassenmethode (den geplanten Job) aufzurufen.
  • Der Thread sollte in der Lage sein, das Ergebnis einer Ausführung zurückzugeben
  • Der Thread sollte in der Lage sein, den Abschluss einer Aufgabe zu melden

Die erste Anforderung bietet Raum für eine lose gekoppelte Implementierung, da sie uns nicht zwingt, eine Schnittstelle wie diese zu implementieren Runnable. Dies erleichtert auch die Integration. Wir können unsere erste Anforderung implementieren, indem wir dem Thread die folgenden Informationen bereitstellen:

  • Der Name der Klasse
  • Der Name der aufzurufenden Methode
  • Die Parameter, die an die Methode übergeben werden sollen
  • Die Parametertypen der übergebenen Parameter

Die zweite Anforderung ermöglicht es einem Client, der den Thread verwendet, das Ausführungsergebnis zu empfangen. Eine einfache Implementierung wäre, das Ergebnis der Ausführung zu speichern und eine Zugriffsmethode wie bereitzustellen getResult().

Die dritte Anforderung hängt etwas mit der zweiten Anforderung zusammen. Das Melden des Abschlusses einer Aufgabe kann auch bedeuten, dass der Client darauf wartet, das Ergebnis der Ausführung zu erhalten. Um diese Funktion zu nutzen, können wir eine Art Rückrufmechanismus bereitstellen. Der einfachste Rückrufmechanismus kann unter Verwendung der java.lang.Object's wait()und der notify()Semantik implementiert werden . Alternativ könnten wir das Observer- Muster verwenden, aber lassen Sie uns die Dinge vorerst einfach halten. Sie könnten versucht sein, die Methode der java.lang.ThreadKlasse zu join()verwenden, aber das funktioniert nicht, da der gepoolte Thread seine run()Methode nie abschließt und so lange ausgeführt wird, wie der Pool sie benötigt.

Nachdem wir unsere Anforderungen fertig haben und eine ungefähre Vorstellung davon haben, wie der Thread-Pool implementiert werden soll, ist es an der Zeit, eine echte Codierung durchzuführen.

At this stage, our UML class diagram of the proposed design looks like the figure below.

Implementing the thread pool

The thread object we are going to pool is actually a wrapper around the thread object. Let's call the wrapper the WorkerThread class, which extends the java.lang.Thread class. Before we can start coding WorkerThread, we must implement the framework requirements. As we saw earlier, we must implement the PoolableObjectFactory, which acts as a factory, to create our poolable WorkerThreads. Once the factory is ready, we implement the ThreadPool by extending the GenericObjectPool. Then, we finish our WorkerThread.

Implementing the PoolableObjectFactory interface

We begin with the PoolableObjectFactory interface and try to implement the necessary lifecycle methods for our thread pool. We write the factory class ThreadObjectFactory as follows:

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

Lassen Sie uns jede Methode im Detail durchgehen:

Methode makeObject()erstellt das WorkerThreadObjekt. Bei jeder Anforderung wird der Pool überprüft, um festzustellen, ob ein neues Objekt erstellt oder ein vorhandenes Objekt wiederverwendet werden soll. Wenn beispielsweise eine bestimmte Anforderung die erste Anforderung ist und der Pool leer ist, ObjectPoolruft die Implementierung die auf makeObject()und fügt WorkerThreadsie dem Pool hinzu.

Die Methode destroyObject()entfernt das WorkerThreadObjekt aus dem Pool, indem sie ein Boolesches Flag setzt und dadurch den laufenden Thread stoppt. Wir werden uns dieses Stück später noch einmal ansehen, aber beachten Sie, dass wir jetzt die Kontrolle darüber übernehmen, wie unsere Objekte zerstört werden.