Połącz zasoby przy użyciu Commons Pool Framework Apache

Łączenie zasobów (zwane także gromadzeniem obiektów) między wieloma klientami to technika używana do promowania ponownego wykorzystania obiektów i zmniejszenia obciążenia związanego z tworzeniem nowych zasobów, co skutkuje lepszą wydajnością i przepustowością. Wyobraź sobie wysokowydajną aplikację serwerową Java, która wysyła setki zapytań SQL, otwierając i zamykając połączenia dla każdego żądania SQL. Lub serwer sieci Web, który obsługuje setki żądań HTTP, obsługując każde żądanie, tworząc osobny wątek. Lub wyobraź sobie tworzenie instancji parsera XML dla każdego żądania analizy dokumentu bez ponownego wykorzystywania instancji. Oto kilka scenariuszy, które gwarantują optymalizację wykorzystywanych zasobów.

Wykorzystanie zasobów może czasami okazać się krytyczne w przypadku aplikacji o dużym obciążeniu. Niektóre znane witryny internetowe zostały zamknięte z powodu niezdolności do obsługi dużych obciążeń. Większość problemów związanych z dużymi obciążeniami można rozwiązać na poziomie makr, korzystając z funkcji klastrowania i równoważenia obciążenia. Obawy pozostają na poziomie aplikacji w odniesieniu do nadmiernego tworzenia obiektów i dostępności ograniczonych zasobów serwera, takich jak pamięć, procesor, wątki i połączenia z bazą danych, które mogą stanowić potencjalne wąskie gardła, a jeśli nie są optymalnie wykorzystywane, mogą spowodować uszkodzenie całego serwera.

W niektórych sytuacjach zasady użytkowania bazy danych mogą wymuszać ograniczenie liczby jednoczesnych połączeń. Ponadto aplikacja zewnętrzna może dyktować lub ograniczać liczbę równoczesnych otwartych połączeń. Typowym przykładem jest rejestr domeny (taki jak Verisign), który ogranicza liczbę dostępnych aktywnych połączeń gniazd dla rejestratorów (np. BulkRegister). Łączenie zasobów okazało się jedną z najlepszych opcji rozwiązywania tego typu problemów i, do pewnego stopnia, pomaga również w utrzymaniu wymaganego poziomu usług dla aplikacji korporacyjnych.

Większość dostawców serwerów aplikacji J2EE udostępnia pulę zasobów jako integralną część swoich kontenerów WWW i EJB (Enterprise JavaBean). W przypadku połączeń z bazami danych dostawca serwera zwykle zapewnia implementację DataSourceinterfejsu, która działa w połączeniu z ConnectionPoolDataSourceimplementacją dostawcy sterownika JDBC (Java Database Connectivity) . ConnectionPoolDataSourceRealizacja służy jako fabryki połączeń menedżera zasobów dla połączonych java.sql.Connectionobiektów. Podobnie instancje EJB bezstanowych komponentów bean sesyjnych, komponentów bean sterowanych komunikatami i encji są gromadzone w kontenerach EJB w celu zwiększenia przepustowości i wydajności. Instancje analizatora składni XML są również kandydatami do łączenia w pule, ponieważ tworzenie instancji analizatora składni pochłania znaczną część zasobów systemu.

Udaną implementacją pulowania zasobów typu open source jest DBCP platformy Commons Pool, komponent puli połączeń z bazą danych firmy Apace Software Foundation, który jest szeroko stosowany w aplikacjach korporacyjnych klasy produkcyjnej. W tym artykule omówię pokrótce wewnętrzne cechy struktury Commons Pool, a następnie wykorzystam ją do zaimplementowania puli wątków.

Najpierw przyjrzyjmy się, co zapewnia framework.

Framework Commons Pool

Framework Commons Pool oferuje podstawową i niezawodną implementację do łączenia dowolnych obiektów. Udostępniono kilka implementacji, ale do celów tego artykułu używamy najbardziej ogólnej implementacji - GenericObjectPool. Używa CursorableLinkedList, która jest implementacją listy podwójnie połączonej (część zbiorów Jakarta Commons), jako bazowej struktury danych do przechowywania obiektów będących w puli.

Ponadto platforma zapewnia zestaw interfejsów, które dostarczają metody cyklu życia i metody pomocnicze do zarządzania, monitorowania i rozszerzania puli.

Interfejs org.apache.commons.PoolableObjectFactorydefiniuje następujące metody cyklu życia, które są niezbędne do implementacji komponentu pulowania:

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

Jak można zauważyć po sygnaturach metod, ten interfejs zajmuje się głównie:

  • makeObject(): Zaimplementuj tworzenie obiektu
  • destroyObject(): Zaimplementuj zniszczenie obiektu
  • validateObject(): Sprawdź poprawność obiektu przed jego użyciem
  • activateObject(): Zaimplementuj kod inicjalizacji obiektu
  • passivateObject(): Zaimplementuj kod uninicjalizacji obiektu

Inny podstawowy interfejs - org.apache.commons.ObjectPool—definiuje następujące metody zarządzania pulą i jej monitorowania:

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

Na ObjectPoolwdrożenie interfejsu, trwa PoolableObjectFactoryjako argument w jego konstruktorów, tym samym przekazując tworzenie obiektów do jej podklas. Nie mówię tu dużo o wzorcach projektowych, ponieważ nie jest to naszym celem. Dla czytelników zainteresowanych spojrzeniem na diagramy klas UML, zobacz Zasoby.

Jak wspomniano powyżej, klasa org.apache.commons.GenericObjectPooljest tylko jedną implementacją org.apache.commons.ObjectPoolinterfejsu. Framework zapewnia również implementacje dla puli obiektów z kluczem, wykorzystując interfejsy org.apache.commons.KeyedObjectPoolFactoryi org.apache.commons.KeyedObjectPool, gdzie można powiązać pulę z kluczem (tak jak w HashMap), a tym samym zarządzać wieloma pulami.

Klucz do skutecznej strategii tworzenia puli zależy od tego, jak skonfigurujemy pulę. Źle skonfigurowane pule mogą być obciążeniem zasobów, jeśli parametry konfiguracyjne nie są dobrze dostrojone. Spójrzmy na kilka ważnych parametrów i ich przeznaczenie.

Szczegóły konfiguracji

Pula może być konfigurowana przy użyciu GenericObjectPool.Configklasy, która jest statyczną klasą wewnętrzną. Alternatywnie moglibyśmy po prostu użyć GenericObjectPoolmetod ustawiających, aby ustawić wartości.

Poniższa lista zawiera szczegóły niektórych dostępnych parametrów konfiguracyjnych GenericObjectPoolimplementacji:

  • maxIdle: Maksymalna liczba uśpionych instancji w basenie bez uwalniania dodatkowych obiektów.
  • minIdle: Minimalna liczba uśpionych instancji w basenie bez tworzenia dodatkowych obiektów.
  • maxActive: Maksymalna liczba aktywnych instancji w puli.
  • timeBetweenEvictionRunsMillis: Liczba milisekund do uśpienia między uruchomieniami wątku wykrywającego bezczynne obiekty. W przypadku wartości ujemnej nie zostanie uruchomiony żaden wątek wykrywający bezczynne obiekty. Tego parametru należy używać tylko wtedy, gdy ma zostać uruchomiony wątek ewictora.
  • minEvictableIdleTimeMillis: Minimalny czas, przez jaki obiekt, jeśli jest aktywny, może siedzieć bezczynnie w puli, zanim będzie kwalifikował się do eksmisji przez eksmitującego bezczynne obiekty. Jeśli podano wartość ujemną, żadne obiekty nie są wykluczane z powodu samego czasu bezczynności.
  • testOnBorrow: Kiedy „prawda”, obiekty są sprawdzane. Jeśli obiekt nie przejdzie weryfikacji, zostanie usunięty z puli, a pula spróbuje pożyczyć inną.

Dla powyższych parametrów należy zapewnić optymalne wartości, aby uzyskać maksymalną wydajność i przepustowość. Ponieważ wzorzec użytkowania różni się w zależności od aplikacji, dostosuj pulę za pomocą różnych kombinacji parametrów, aby uzyskać optymalne rozwiązanie.

Aby dowiedzieć się więcej o puli i jej elementach wewnętrznych, zaimplementujmy pulę wątków.

Proponowane wymagania dotyczące puli wątków

Załóżmy, że powiedziano nam, abyśmy zaprojektowali i zaimplementowali komponent puli wątków dla programu planującego zadania, aby wyzwalać zadania według określonych harmonogramów i informować o ich zakończeniu i być może wyniku wykonania. W takim scenariuszu celem naszej puli wątków jest zebranie wymaganej liczby wątków i wykonanie zaplanowanych zadań w niezależnych wątkach. Wymagania są podsumowane w następujący sposób:

  • Wątek powinien mieć możliwość wywołania dowolnej metody klasy (zaplanowanego zadania)
  • Wątek powinien móc zwrócić wynik wykonania
  • Wątek powinien być w stanie zgłosić zakończenie zadania

Pierwsze wymaganie zapewnia zakres dla luźno powiązanej implementacji, ponieważ nie zmusza nas do implementacji interfejsu takiego jak Runnable. Ułatwia również integrację. Nasze pierwsze wymaganie możemy zrealizować, podając wątkowi następujące informacje:

  • Nazwa klasy
  • Nazwa metody, która ma zostać wywołana
  • Parametry, które mają zostać przekazane do metody
  • Typy parametrów przekazanych parametrów

Drugie wymaganie umożliwia klientowi korzystającemu z wątku uzyskanie wyniku wykonania. Prostą implementacją byłoby przechowywanie wyniku wykonania i zapewnienie metody dostępu, takiej jak getResult().

Trzeci wymóg jest w pewnym stopniu powiązany z drugim wymaganiem. Zgłoszenie wykonania zadania może również oznaczać, że klient czeka na wynik wykonania. Aby obsłużyć tę możliwość, możemy zapewnić jakąś formę mechanizmu wywołania zwrotnego. Najprostszy mechanizm wywołania zwrotnego można zaimplementować za pomocą java.lang.Objectznaków wait()i notify()semantyki. Alternatywnie moglibyśmy użyć wzorca Observer , ale na razie zachowajmy prostotę. Możesz ulec pokusie, aby użyć metody java.lang.Threadklasy join(), ale to nie zadziała, ponieważ wątek w puli nigdy nie kończy swojej run()metody i działa tak długo, jak potrzebuje tego pula.

Teraz, gdy mamy już gotowe wymagania i przybliżony pomysł, jak zaimplementować pulę wątków, czas na prawdziwe kodowanie.

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

Przeanalizujmy szczegółowo każdą metodę:

Metoda makeObject()tworzy WorkerThreadobiekt. Dla każdego żądania pula jest sprawdzana, aby zobaczyć, czy ma zostać utworzony nowy obiekt, czy też ma zostać ponownie użyty istniejący obiekt. Na przykład, jeśli określone żądanie jest pierwszym żądaniem, a pula jest pusta, ObjectPoolimplementacja wywołuje makeObject()i dodaje WorkerThreaddo puli.

Metoda destroyObject()usuwa WorkerThreadobiekt z puli, ustawiając flagę logiczną, a tym samym zatrzymując działający wątek. Przyjrzymy się temu fragmentowi ponownie później, ale zauważmy, że teraz przejmujemy kontrolę nad sposobem niszczenia naszych obiektów.