r03-05.doc

(334 KB) Pobierz
Szablon dla tlumaczy

 

W tym rozdziale:

·         Alternatywa apletu

·         Odnawianie (powtórne ładowanie) apletu

·         Inicjalizacja i usuwanie

·         Model trój-wątkowy (Single-Thread Model)

·         Przetwarzanie drugoplanowe

·         Ładowanie i uruchamianie

·         Buforowanie podręczne po stronie klienta

·         Buforowanie podręczne po stronie serwera

Rozdział 3.

Czas istnienia (cykl życia) apletu

Czas istnienia (cykl życia) apletu jest jednym z bardziej interesujących aspektów apletów. Czas istnienia jest hybrydą czasów istnienia używanych w środkach programowania CGI oraz środkach programowania niskiego poziomu WAI/NSAPI i ISAPI, tak jak zostało to omówione w rozdziale1 „Wprowadzenie”.

Alternatywa apletu

Czas istnienia (cykl życia) apletów pozwala ich pojemnikom na odniesienie się zarówno do wydajności, jak i do problemów związanych z CGI oraz do problemów dotyczących bezpieczeństwa nisko-poziomowych środków programowania serwerów API. Pojemniki apletów uruchamiają zwykle aplety wszystkie razem, w jednej maszynie wirtualnej Javy (JVM). Dzięki umiejscowieniu wszystkich apletów w tej samej JVM mogą one skutecznie wymieniać dane między sobą, jednak co się tyczy ich danych „prywatnych” — język Java nie daje możliwości wglądu jednemu apletowi w dane znajdujące się na drugim. Aplety mogą istnieć w JVM-ie pomiędzy zleceniami — jako kopie obiektów. Dzięki temu zajęte jest mniej pamięci niż w przypadku pełnej procedury, a aplety są nadal w stanie utrzymać odniesienia do zewnętrznych zasobów. Cykl życia apletów nie jest wielkością stałą. Jedyną rzeczą niezmienną i konieczną w tym cyklu jest to, iż pojemnik apletu musi przestrzegać następnych zasad:

 

  1. Stworzyć oraz uruchomić aplet
  2. Obsłużyć wywołania usługi od klientów
  3. Usunąć aplet a następnie go przywrócić

 

Jest rzeczą całkowicie naturalną w przypadku apletów, iż są one ładowane, tworzone konkretyzowane w swojej własnej maszynie wirtualnej Javy — tylko po to aby być usuniętymi i odtworzonymi nie obsłużywszy żadnych zleceń od klientów lub po obsłużeniu tylko jednego takiego zlecenia. Jednakże aplety zachowujące się w taki sposób nie utrzymają się długo na rynku. W tym rozdziale omówimy najbardziej popularne oraz czułe realizacje czasów istnienia apletów HTTP.

Pojedyncza maszyna wirtualna Javy

Większość pojemników apletowych wdraża wszystkie aplety do jednej JVM w celu maksymalizacji zdolności apletów do wymiany informacji (wyjątkiem są tutaj pojemniki wyższej klasy, które realizują rozproszone wywołanie apletu na wielu serwerach wewnętrznych, tak jak zostało to omówione w rozdziale 12 „Serwery Przedsiębiorstw oraz J2EE”.

Wykonania wyżej wspomnianej pojedynczej maszyny wirtualnej Javy mogą by różne na różnych serwerach:

·         Na serwerze napisanym w Javie, takim jak np. „Apache Tomcat”, sam serwer może wywoływać w JVM-ie wraz ze swoimi apletami.

·         Na pojedynczo przetwarzającym, wielo-wątkowym serwerze WWW, zapisanym w innym języku, wirtualna maszyna Javy może zostać zawarta w procedurze serwera. JVM jako część procedury serwera zwiększa wydajność ponieważ aplet staje się w pewnym sensie, kolejnym rozszerzeniem serwera API niskiego poziomu. Serwer taki może wywołać aplet z nieskomplikowanym połączeniem kontekstu, może również dostarczyć informacje o zleceniach poprzez wywołania metod bezpośrednich.

 

·         Wieloprocedurowy serwer WWW (który uruchamia kilka procedur, aby obsłużyć zlecenia) właściwie nie może zawrzeć JVM w swojej procedurze ponieważ takiej nie posiada. Ten typ serwerów zwykle uruchamia zewnętrzny JVM, którego procedury może współdzielić. Taki sposób oznacza, iż każde wejście do apletu wiązać się będzie ze skomplikowanym połączeniem kontekstu przypominającym FastCGI. Jednakże wszystkie aplety będą nadal dzieliły tą samą zewnętrzną procedurę.

Na szczęście, z perspektywy apletów (a tym samym z naszej — jako ich twórców) wdrażanie serwerów nie ma większego znaczenia ponieważ zachowują się one zawsze w te sam sposób.

Trwałość kopii

Tak jak to zostało opisane wcześniej, aplety istnieją pomiędzy zleceniami jako kopie obiektów. Inaczej mówiąc w czasie ładowania kodu dla apletu, serwer tworzy pojedynczą kopię. Ta pojedyncza kopia obsługuje wszystkie zlecenia, utworzone z apletu. Poprawia to wydajność w trzy następujące sposoby:

·         zajmowana powierzchnia pamięci jest mała;

·         pozwala to wyeliminować obciążenie tworzenia obiektu (w przeciwnym wypadku konieczne byłoby utworzenie nowego obiektu apletu) aplet może być już ładowany w maszynie wirtualnej, kiedy zlecenie dopiero wchodzi, pozwalając mu na rozpoczęcie wywoływania natychmiast;

·         umożliwia trwanie — aplet może mieć wszystko, czego może potrzebować podczas obsługi zlecenia, już załadowane np. połączenie z bazą danych może zostać ustanowione raz i używane wielokrotnie; z takiego połączenia może korzystać wiele serwerów. Kolejnym przykładem może tutaj być aplet koszyka zakupów, który ładuje do pamięci listę cen wraz z informacją o ostatnio połączonych klientach. Niektóre serwery w sytuacji, kiedy otrzymują to samo zlecenie po raz drugi umieszczają całe strony w schowku, celem zaoszczędzenia czasu.

Aplety nie tylko trwają pomiędzy zleceniami, lecz także wykonują wszystkie wątki stworzone przez siebie. Taka sytuacja nie jest może zbyt korzystna w przypadku apletu „run-of-the-mill”, jednakże daje interesujące możliwości. Rozważmy sytuację, w której podrzędny wątek przeprowadza pewne kalkulacje, podczas, gdy inne wyświetlają ostatnie rezultaty. Podobnie jest w przypadku apletu animacyjnego, w którym jeden wątek zamienia obraz, a inny nanosi kolory.

Liczniki

W celu przedstawienia cyklu życia (czasu istnienia apletu) posłużymy się prostym przykładem. Przykład 3.1 ukazuje serwer, który zlicza i wyświetla liczbę połączeń się z nim. Dla uproszczenia wynik przedstawiany jest jako zwykły tekst (kod dla wszystkich przykładów dostępny jest w internecie — patrz Wstęp)

 

Przykład 3.1. Przykładowy prosty licznik

 

import java.io.*;

import javax.servlet.*;

import javax.servlet.http.*;

 

public class SimpleCounter  extends HttpServlet  {

 

          int  count = 0;

 

          public  void doGet (HttpServletRequest  req, HttpServletResponse res)

                throws  ServletException, IOException  {

res.setContentType(„text / zwykły”);

PrintWriter out  = res.getWriter();

   count++;

   out.println ("Od załadowania z apletem łączono się" +

                                         count + " razy.");

}

}

Kod jest prosty — po prostu wyświetla oraz zwiększa kopię zmiennej zwanej count, jednakże dobrze ukazuje „potęgę” trwałości. Kiedy serwet ładuje ten aplet tworzy pojedynczą kopię celem obsłużenia wszystkich zleceń, złożonych na ten aplet, dlatego właśnie kod bywa taki prosty. Takie same kopie zmiennych występują pomiędzy wywołaniami, oraz w przypadku wszystkich wywołań.

Liczniki zsynchronizowane

Z punktu widzenia projektantów apletów każdy klient, to kolejny wątek, który wywołuje aplet poprzez metody takie jak: service(), doGet (), doPost(), tak jak to pokazuje przykład 3.1*.

 

Rysunek 3.1. Wiele wątków — jedna kopia apletu

 

Jeżeli nasze aplety odczytują tylko zlecenia, piszą w odpowiedziach i zapisują informacje w lokalnych zmiennych, (czyli w zmiennych określonych w metodzie) nie musimy obawiać się interakcji pomiędzy wątkami. Jeżeli informacje zostają zapisane w zmiennych nielokalnych (czyli w zmiennych określonych w klasie, lecz poza szczególną metodą) musimy być wtedy świadomi, iż każdy z wątków klienckich może operować tymi zmiennymi apletu. Bez odpowiednich środków ostrożności sytuacja taka może spowodować zniszczenie danych oraz sprzeczności. I tak np. jeżeli aplet SimpleCounter założy fałszywie, że przyrost na liczniku oraz wyprowadzenie są przeprowadzanie niepodzielnie (bezpośrednio jeden po drugim, nieprzerwanie), to jeżeli dwa zlecenia zostaną złożone do SimpleCounter prawie w tym samym czasie, możliwe jest wtedy, że każdy z nich wskaże tą samą wartość dla count. Jak? Wyobraźmy sobie, że jeden wątek zwiększa wartość dla count i zaraz po tym, zanim jeszcze pierwszy watek wypisze wynik count, drugi wątek również zwiększa wartość. W takim przypadku, każdy z wątków wskaże tą samą wartość, po efektywnym zwiększeniu jej o 2أ.

 

Dyrektywa wykonania wygląda mniej więcej w ten sposób:

 

count++           // Wątek 1

 

count++           // Wątek 2

 

out.println       // Wątek 3

 

out.println       // Wątek 4

 

W tym przypadku ryzyko sprzeczności nie stanowi poważnego zagrożenia, jednakże wiele innych apletów zagrożonych jest poważniejszymi błędami. W celu zapobieżenia temu typowi błędów oraz sprzecznościom, które im towarzyszą, możemy dodać jeden lub więcej synchronicznych bloków do kodu. Jest gwarancja, że wszystko, co znajduje się w bloku synchronicznym lub w metodzie synchronicznej nie będzie wywoływane przez inny wątek. Zanim jakikolwiek z wątków rozpocznie wywoływanie kodu synchronicznego musi otrzymać monitor (zamek) na określoną kopie obiektu. Jeżeli monitor ma już inny wątek np. z powodu tego, że wywołuje on ten sam blok synchroniczny, lub inny tym samym monitorem, wtedy pierwszy wątek musi zaczekać. Działa to na zasadzie łazienki na stacji benzynowej, zamykanej na klucz (zawieszany zwykle na dużej, drewnianej desce), którym w naszym przypadku będzie monitor. Wszystko to dzieje się dzięki samemu językowi tak więc obsługa jest łatwa. Synchronizacja jednakże powinna być używana tylko w ostateczności. W przypadku niektórych platform sprzętowych otrzymanie monitora za każdym razem, kiedy wchodzimy do kodu synchronicznego wymaga wiele wysiłku, a co ważniejsze w czasie, kiedy jeden wątek wywołuje kod synchroniczny, pozostałe mogą być blokowane do zwolnienia monitora.

Dla SimpleCounter istnieją cztery sposoby rozwiązywania potencjalnych problemów. Po pierwsze możemy dodać hasło zsynchronizowane z sygnaturą doGet():

 

       public synchronized void doGet (HttpServletRequest req,

                                       HttpServletResponse res)       

 

Taka sytuacja gwarantuje zgodność synchronizacji całej metody, używa się w tym celu kopii apletu, jako monitora. Nie jest to w rzeczywistości najlepsza metoda, ponieważ oznacza to, iż aplet może w tym samym czasie obsłużyć tylko jedno zlecenie GET.

Drugim sposobem jest zsynchronizowanie tylko dwóch wierszy, które chcemy wywołać niepodzielnie.

 

PrintWriter out = res.getWriter();

synchronized(this) {

  count++;

  out.println ("Od załadowania z apletem łączono się" +

                     count + " razy.");

}

  

Powyższa technika działa lepiej, ponieważ ogranicza czas, który aplet spędza w swoim zsynchronizowanym bloku, osiągając ten sam cel zgodności w wyniku liczenia. Prawdą jest, iż technika ta nie różni się specjalnie od pierwszego sposobu.

Trzecim sposobem poradzenia sobie z potencjalnymi problemami jest utworzenie synchronicznego bloku, który wykonywał będzie wszystko, co musi być wykonane szeregowo, a następnie wykorzystanie poza blokiem synchronicznym. W przypadku naszego apletu, liczącego możemy zwiększyć wartość liczoną (count) w bloku synchronicznym, zapisać zwiększoną wartość do lokalnej zmiennej (zmiennej określonej wewnątrz metody), a następnie wyświetlić wartość lokalnej zmiennej poza blokiem synchronicznym:

PrintWriter out = res.getWriter();

int local_count;

synchronized(this) {

  local_count= ++count;

}

     out.println ("Od załadowania z apletem łączono się" +

                  localcount + " razy.");

Powyższa zmienna zawęża blok synchroniczny do najmniejszych, możliwych rozmiarów zachowując przy tym zgodność liczenia.

Celem zastosowania czwartej, ostatniej z metod musimy zadecydować, czy chcemy ponieść konsekwencje zignorowania wyników synchronizacji. Czasem bywa i tak, że konsekwencje te są całkiem znośne. Dla przykładu, zignorowanie synchronizacji może oznaczać, że klienci otrzymają wynik trochę niedokładny. Trzeba przyznać, iż to rzeczywiście nie jest wielki problem. Jeżeli jednak oczekiwano by od apletu liczb dokładnych, wtedy sprawa wyglądałaby trochę gorzej.

Mimo, iż nie jest to opcja możliwa do zastosowania na omawianym przykładzie, to na innych apletach możliwa jest zamiana kopii zmiennych na zmienne lokalne. Zmienne lokalne są niedostępne dla innych wątków i tym samym nie muszą być dokładnie strzeżone przed zniszczeniem. Jednocześnie zmienne lokalne nie istnieją pomiędzy zleceniami, tak więc nie możemy ich użyć do utrzymywania stałego stanu naszego licznika.

 

Liczniki całościowe

Model „jeden egzemplarz na jeden aplet” jest sprawą do omówienia ogólnego. Prawda jest taka, że każda zarejestrowana nazwa (lecz nie każde URL-owe dopasowanie do wzorca) dla apletu jest związana z jedną kopią apletu. Nazwa używana przy wchodzeniu do apletu określa, która kopia obsłuży zlecenie. Taka sytuacja wydaje się być sensowna, ponieważ klient powinien kojarzyć odmienne nazywanie apletów z ich niezależnym działaniem. Osobne kopie są ponadto wymogiem dla apletów zgodnych z parametrami inicjalizacji, tak jak to zostało omówione dalej w tym rozdziale.

Nasz przykładowy SimpleCounter posługuje się kopią liczenia zmiennej przy zliczaniu liczby połączeń z nim wykonanych. Jeżeli byłaby potrzeba liczenia wszystkich kopii (a tym samym wszystkich zarejestrowanych nazw) możliwe jest użycie klasy zmiennej statycznej.

Zmienne takie są wspólne dla wszystkich kopii klasy. Przykład 3.2 ukazuje liczbę wejść na aplet, liczbę kopii utworzonych przez serwer (na jedną nazwę) oraz całkowitą liczbę połączeń z tymi kopiami.

 

Przykład 3.2. Licznik całościowy

 

import java.io.*;

import java.util.*;

import javax.servlet.*;

import javax.servlet.http.*;

 

  public class HolisticCounter  extends HttpServlet  {

 

      static int  Classcount = 0;   // dotyczy wszystkich kopii

      int  count = 0;            // oddzielnie dla każdego apletu

      static Hashtable instances = new Hashtable();   // również dotyczy wszystkich kopii

 

 

public  void doGet (HttpServletRequest  req, HttpServletResponse res)

                            throws  ServletException, IOException  {

         res.setContentType ("text / zwykły");

          PrintWriter out  = res.getWriter();

 

          count++;

          out.println ("Od załadowania z apletem łączono się" +

         count + " razy.");

 

     // utrzymuj ścieżkę liczenia poprzez wstawienie odwołania do niej

     // kopia w tablicy przemieszczania. Powtarzające się hasła są

     // ignorowane

 

          // Metoda size()odsyła liczbę kopii pojedynczych, umieszczonych w pamięci

        instances.put(this, this);

         out.println ("Aktualnie jest" + instances.size() + "razy");

 

         classCount++

    out.println ("Licząc wszystkie kpoie, z apletem tym " + "łączono       

                                  "łączono się" + classCount + "razy")

 

       }

     }

Przedstawiony licznik całościowy — Holistic Counter, śledzi liczbę połączeń własnych przy pomocy zmiennej kopii count, liczbę połączeń wspólnych za pomocą zmiennej klasy oraz liczbę kopii za pomocą tablicy asocjacyjnej — instances (kolejny wspólny element, który musi być zmienną klasy). Widok przykładu ukazuje rysunku 3.2.

Rysunek 3.2. Widok licznika całościowego

Odnawianie (powtórne ładowanie) apletu

Jeśli ktoś próbował używać umówionych liczników, we własnym zakresie być może zauważył, iż z każdą kolejną rekompilacją liczenie zaczyna się automatycznie od 1. Wbrew pozorom to nie defekt, tylko właściwość. Większość serwerów odnawia (powtórnie ładuje) aplety, po tym jak zmieniają się ich pliki klasy (pod domyślnym katalogiem apletów WEB-INF/classes). Jest to procedura wykonywana na bieżąco, która znacznie przyśpiesza cyklu testu rozbudowy oraz pozwala na przedłużenie czasu sprawnego działania serwera.

Odnawianie apletu może wydawać się proste, jednak wymaga dużego nakładu pracy. Obiekty ClassLoader zaprojektowane są do jednokrotnego załadowania klasy.

Aby obejść to ograniczenie i wielokrotnie ładować aplety, serwery używają własnych programów ładujących, które ładują aplety ze specjalnych katalogów, takich jak WEB-INF/classes.

Kiedy serwer wysyła zlecenie do apletu najpierw sprawdza, czy plik klasy apletu zmienił się na dysku. Jeżeli okaże się, że tak wtedy serwer nie będzie już używał programu ładującego starej wersji pliku tylko utworzy nowa kopię własnego programu ładującego klasy — celem załadowania nowej wersji. Niektóre serwery poprawiają wydajność poprzez sprawdzanie znaczników modyfikacji czasu tylko co jakiś czas lub na wyraźne żądanie administratora.

W wersjach Interfejsów API sprzed wersji 2.2, chwyt z programem ładującym klasy skutkował tym, że inne aplety ładowane były przez odmienne programy ładujące — co skutkowało czasem zgłoszeniem ClassCastException jako wyjątku, kiedy aplety wymieniały informacje (ponieważ klasa załadowana przez jeden program ładujący nie jest tym samym, co klasa ładowana przez inny, nawet jeżeli dane dotyczące klasy są identyczne).

Na początku Interfejsu API 2.2 jest gwarancja, że problemy z ClassCastException nie pojawi się dla apletów w tym samym kontekście. Tak więc obecnie większość wdrożeń ładuje każdy kontekst aplikacji WWW w jednym programie ładującym klasy, oraz używa nowego programu ładującego do załadowania całego kontekstu, jeżeli jakikolwiek aplet w kontekście ulegnie zmianie.

Skoro więc wszystkim apletom oraz klasom wspomagającym w kontekście zawsze odpowiada ten sam program ładujący, nie należy się więc obawiać żadnych nieoczekiwanych ClassCastException podczas uruchamiania. Powtórne ładowanie całego kontekstu powoduje mały spadek wydajności, który jednakże występuje tylko podczas tworzenia.

Powtórne ładowanie (odnawianie) klasy nie jest przeprowadzane tylko wtedy, kiedy zmianie ulega klasa wspomagająca. Celem większej efektywności określenia, czy jest konieczne odnawianie kontekstu, serwery sprawdzają tylko znaczniki czasu apletów klasy. Klasy wspomagające w WEB-INF/classes mogą być także powtórnie załadowane, kiedy kontekst jest odnowiony, lecz jeżeli klasa wspomagająca jest jedyną klasą do zmiany, serwer tego prawdopodobnie nie zauważy.

Odnowienie apletu nie jest także wykonywane dla wszystkich klas (apletu lub innych) znajdujących się w ścieżce klasy serwerów. Klasy takie ładowane są przez rdzenny (pierwotny) program ładujący, a nie własny, konieczny do powtórnego załadowania. Klasy te są również ładowane jednorazowo i przechowywane w pamięci nawet wtedy, gdy ich pliki ulegają zmianie. Jeżeli chodzi o klasy globalne (takie jak klasy użyteczności com.oreilly.servlet) to najlepiej jest umieścić je gdzieś na ścieżce klasy, gdzie unikną odnowienia. Przyśpiesza to proces powtórnego ładowania oraz pozwala apletom w innych kontekstach wspólnie używać tych obiektów bez ClassCastException.

Metody „Init” i „Destroy”

Tak jak zwykłe aplety, aplety wykonywane na serwerach mogą określać metody init() i destroy(). Serwer wywołuje metodę init() po skonstruowaniu kopii apletu, jednak zanim jeszcze aplet obsłu...

Zgłoś jeśli naruszono regulamin