Servlets und Probleme mit Threads

(Auszug aus "Java und XSLT" von Eric M. Burke)

Ein Servlet muß in der Lage sein, mehrere Clients zur gleichen Zeit zu bedienen. Die eingebaute Multithread-Fähigkeit von Java ist deshalb ein Hauptgrund, warum es sich für Serveranwendungen anbietet. Dies gilt besonders im Vergleich zum traditionellen CGI-Modell. Natürlich gibt es hier auch Tradeoffs. Es stellt eine ziemliche Herausforderung dar, Programme zu schreiben, die mit mehreren nebenläufigen Tasks umgehen können, ohne Daten zu beschädigen. Idealerweise sollen Sie auf dieser Seite für die Problematik des Einsatzes von Threads bei Servlets sensibilisiert werden.

Das Modell des Einsatzes von Threads bei Servlets

Im Standard-Servlet-Modell stellt der Client eine Anfrage über die Methode service( ) des Servlets. In der Klasse HttpServlet bestimmt die Methode service( ) die Art der HTTP-Anfrage und ruft entsprechend die Methode doGet( ) oder doPost( ) auf. Falls mehrere Clients eine Anfrage stellen, bedienen diese Methoden jeden Client in einem eigenen Thread. Da die meisten Servlets Unterklassen von HttpServlet sind, müssen Sie also darauf achten, daß die Methoden service( ), doGet( ) und doPost( ) mehrere Clients nebenläufig bedienen können.

Bevor irgendeine Anfrage abgearbeitet werden kann, wird die Methode init( ) des Servlets aufgerufen. Entsprechend der Spezifikation der Servlet-API darf diese Methode von nur einem Thread aufgerufen werden und muß erfolgreich beendet werden, bevor weitere Threads die Methode service( ) aufrufen dürfen. Deshalb muß man sich innerhalb der Methode init( ) keine Gedanken um Probleme mit Threads machen. Danach wird es schon problematischer.

Eine einfache Möglichkeit, eine Methode so zu implementieren, daß sie sicher mit Threads umgehen kann, ist diese als synchronized zu deklarieren. Die Methode doGet( ) würde wie folgt aussehen:

protected synchronized void doGet(HttpServletRequest request,
     HttpServletResponse response) throws IOException, ServletException {
   ...
}

Das Schlüsselwort synchronized stellt sicher, daß jeder Thread, der die Methode aufrufen will, zuerst eine Sperre auf das Servlet-Objekt anfordert. Nachdem der erste Client die Sperre erhalten hat und die Methode ausführt, müssen alle anderen auf deren Freigabe warten. Benötigt die Abarbeitung der Methode doGet( ) 0,5 Sekunden und wollen 100 weitere Nutzer auf sie zugreifen, bedeutet das eine Minute Wartezeit für viele Besucher Ihrer Site, denn Sie stehen in einer Warteschlage, um die Sperre zu erhalten.

Das ist natürlich nicht wünschenswert. Eine andere Möglichkeit ist, daß Ihr Servlet das Interface javax.servlet.SingleThreadModel wie folgt implementiert:

public class MeinServlet extends HttpServlet implements SingleThreadModel {
...
}

Das Interface SingleThreadModel ist ein Marker-Interface, d.h. es deklariert keine Methoden. Es ist nur ein Hinweis für den Servlet-Container, daß das Servlet nicht sicher im Umgang mit Threads ist und immer nur eine Anfrage in der Methode service( ) bearbeiten kann. Ein typischer Servlet-Container erzeugt dann einen Pool von Servlet-Instanzen, wobei jede Instanz immer nur eine Anfrage bearbeitet.

Das ist eine etwas bessere Lösung, als die Methoden doGet( ) und doPost( ) zu synchronisieren. Es bedeutet aber, daß mehrere Kopien des Servlets instantiiert werden. Das resultiert in einem höheren Speicherbedarf und stellt trotzdem nicht sicher, daß alle Probleme bei der Verwendung von Threads richtig gelöst werden. Nebenläufige Änderungen an einer gemeinsamen Ressource, wie einer Datei oder einer als static deklarierten Variablen, werden z.B. nicht verhindert.

Tips zur sicheren Verwendung von Threads

Die meisten thread-bezogenen Probleme im Umgang mit Servlets treten auf, wenn zwei oder mehr Threads Änderungen an einer Ressource vornehmen. Das könnte bedeuten, daß mehrere Threads versuchen, eine Datei zu modifizieren oder den Wert einer gemeinsamen Variable zur gleichen Zeit zu ändern. Das führt zu unvorhersagbarem Verhalten und ist nur schwer zu diagnostizieren. Ein weiteres Problem ist der sogenannte Deadlock, bei dem zwei Threads eine Ressource verwenden wollen, wobei jeder eine Sperre besitzt, die der jeweils andere benötigt. Wie bereits erwähnt, spielt auch Performance eine Rolle, da die Synchronisation des Zugriffs auf eine Methode erhebliche Leistungseinbußen mit sich bringen kann.

Der beste Ansatz, die Sicherheit eines Servlets in einer Multithread-Umgebung zu garantieren, ist nicht das Interface SingleThreadModel zu verwenden, sondern den Zugriff auf die Methode service( ) zu synchronisieren. So kann Ihr Servlet mehrere Clientanfragen zur selben Zeit bearbeiten. Das bedeutet aber, daß Sie Situationen vermeiden müssen, in denen mehr als ein Thread eine gemeinsame Ressource ändern kann. Die folgenden Tips sollen ein paar Anregungen geben.

Tip 1: Lokale Variablen sind sicher

Objektvariablen in einem Servlet bringen oft Probleme. Betrachten wir den folgenden Code:

public class HomeServlet extends HttpServlet {
  private Customer currentCust;
  Servlets und Probleme mit Threads | 227
   protected void doGet(HttpServletRequest request,
     HttpServletResponse response) throws IOException,
     ServletException {
    HttpSession session = request.getSession(true);
    currentCust = (Customer) session.getAttribute("cust");
    currentCust.setLastAccessedTime(new Date( ));
    ...
  }
}

In diesem Beispiel wird das Feld currentCust durch HttpSession ermittelt, wenn ein neuer Client die Methode doGet( ) aufruft. Wenn nun ein anderer Thread diese Methode nur eine kurze Zeit später aufruft, wird das Feld currentCust überschrieben, noch bevor der erste Thread die Methode beendet hat. Tatsächlich könnten sehr viele Threads die Methode doGet( ) fast zur selben Zeit betreten, wobei jeder die Referenz currentCust ändert.

Als einfache Lösung kann currentCust als lokale Variable deklariert werden:

public class HomeServlet extends HttpServlet {
  protected void doGet(HttpServletRequest request,
     HttpServletResponse response) throws IOException,
     ServletException {
   HttpSession session = request.getSession(true);
   Customer currentCust = (Customer) session.getAttribute("cust");
   currentCust.setLastAccessedTime(new Date( ));
   ...
 }
}

Das Problem ist damit gelöst, weil in Java jeder Thread mit einer eigenen Kopie einer lokalen Variablen arbeitet.

Tip 2: Unveränderliche Objekte sind sicher

Immer wenn zwei oder mehr Threads gleichzeitig Änderungen an einem Objekt vornehmen, kann es zu einer Art Wettlauf kommen. Betrachten wir das folgende Beispiel:

public class Person {
  private String firstName;
  private String lastName;
  
  public void setName(String firstName, String lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
 ...get-Methoden weggelassen
}

Wenn zwei Threads die Methode setName( ) zu ungefähr derselben Zeit aufrufen, kann das folgende Szenario auftreten:

  1. Thread »A« setzt den Vornamen auf »Bill«, wird aber von Thread »B« unterbrochen.
  2. Thread »B« setzt den Vornamen auf »George« und den Nachnamen auf »Bush«.
  3. Thread »A« ist wieder an der Reihe und setzt den Nachnamen auf »Clinton«.

Nun ist der Name der Person George Clinton, was natürlich nicht beabsichtigt war. Auch wenn man die Methode setName( ) als synchronized deklariert, müßten zusätzlich alle get-Methoden als synchronized deklariert werden.

Eine andere Möglichkeit ist es, das Objekt unveränderlich zu machen. Die Klasse Person müßte für diesen Ansatz wie folgt definiert werden:

public class Person {
  private String firstName;
  private String lastName;
  
  public Person(String firstName, String lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  
  public String getFirstName( ) { return this.firstName; }
  public String getLastName( ) { return this.lastName; }
}

Da Instanzen der Klasse Person gar nicht geändert werden können, müssen ihre Methoden auch nicht synchronized sein. Das macht die Objekte starr und erlaubt ein nebenläufiges Auslesen durch mehrere Threads. Der einzige Nachteil ist, daß die Objekte, nachdem sie einmal erzeugt wurden, nicht mehr geändert werden können. Statt dessen kann man aber einfach ein neues Objekt des Typs Person erzeugen, wenn Änderungen nötig sind. Das ist im wesentlichen der Ansatz, nach dem java.lang.String arbeitet.

Unveränderliche Objekte sind nicht immer das Mittel der Wahl, stellen aber eine nützliche Technik für kleinere Hilfsklassen dar, wie sie in nahezu jeder Anwendung vorkommen.

Tip 3: Stellen Sie nur einen Zugangspunkt zur Verfügung

Wenn Sie es mit einer Instanz einer mehrfach genutzten Ressource, wie z.B. einer zu ändernden Datei, zu tun haben, sollten Sie darüber nachdenken, eine Fassade um diese Ressource aufzubauen. Das kann eine einzelne Klasse sein, die den Zugriff auf diese Ressource kontrolliert und so als Synchronisationspunkt in Ihrem Programm dient. Das folgende Beispiel demonstriert, wie man eine Fassade um eine Datenquelle mit Customer-Objekten aufbauen kann. Die Klasse Customer soll unveränderlich sein, so daß eine Instanz von Customer nur über die hier definierte API manipuliert werden kann:

public class CustomerSource {
  public static synchronized Customer getCustomer(String id) {
    // Lese die Kundendaten aus einer Datei
    // oder einer Datenbank...
  }
  
  public static synchronized Customer createCustomer( ) {
    // erzeuge einen neuen Kunden in der Datei
    // bzw. der Datenbank und liefere ihn zurück...
  }
  
  public static synchronized void deleteCustomer(String id) {
    // ...
  }
}

Das ist ein einfacher Ansatz, der für kleine Anwendungen gut geeignet ist. Die Methoden doGet( ) bzw. doPost( ) können die Klasse CustomerSource verwenden, ohne irgendwelche Daten zu beschädigen. Wenn die Methoden von CustomerSource jedoch langsam sind, behindern sie wiederum die Ausführung, so daß Clients darauf warten, auf die zugrundeliegende Datenstruktur zuzugreifen.

Tip 4: Verstehen Sie das Interface von Templates

Mehrere Threads können sich eine Instanz von javax.xml.transform.Templates teilen. Diese können also als Objektvariablen eines Servlets Verwendung finden:

public class MyServlet extends HttpServlet {
  private Templates homePageStylesheet;
  ...
}

Instanzen von javax.xml.transform.Transformer sind hingegen nicht sicher und sollten deshalb als lokale Variablen der Methoden doGet() bzw. doPost() deklariert werden:

public class MyServlet extends HttpServlet {
  private Templates homePageStylesheet;
  
  public void init( ) throws UnavailableException {
   ... erzeuge eine Instanz von Templates
  }
  
  protected void doGet( ) {
    Transformer trans = homePageStylesheet.newTransformer( );
    ... benutze die Instanz von Transformer als lokale Variable
  }
}

   

<< zurück vor >>

 

 

 

Tipp der data2type-Redaktion:
Zum Thema Java & XSLT bieten wir auch folgende Schulungen zur Vertiefung und professionellen Fortbildung an:

Copyright © 2002 O'Reilly Verlag GmbH & Co. KG
Für Ihren privaten Gebrauch dürfen Sie die Online-Version ausdrucken.
Ansonsten unterliegt dieses Kapitel aus dem Buch "Java und XSLT" denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

O’Reilly Verlag GmbH & Co. KG, Balthasarstraße 81, 50670 Köln, kommentar(at)oreilly.de