Schreiben Sie threadsichere Servlets

Wenn Sie Webanwendungen in Java schreiben, ist das Servlet Ihr bester Freund. Unabhängig davon, ob Sie Java ServerPages (JSP) oder einfache Servlets schreiben, sind Sie sowohl dem Servlet ausgeliefert als auch dem glücklichen Empfänger dessen, was das Servlet zu bieten hat. Schauen wir uns also das Servlet genauer an.

Wie wir wissen, sind wir beim Erstellen einer Website hauptsächlich an den beiden Methoden des Servlets interessiert: doPost()und doGet(). Die Unterstreichungsmethode für beide Methoden ist service(). Später werde ich mir diese Methoden und ihre Beziehung zur Thread-Sicherheit genauer ansehen, aber zuerst wollen wir das Konzept eines Threads überprüfen.

Ein Thread ist ein einzelner Ausführungsprozess. mit anderen Worten, ein individueller, sequentieller Steuerungsfluss innerhalb eines Programms. Wenn wir sagen, dass ein Programm Multithreading ist, bedeutet dies nicht, dass das Programm zwei separate Instanzen gleichzeitig ausführt (als ob Sie das Programm zweimal gleichzeitig über die Befehlszeile ausgeführt hätten). Wir sagen vielmehr, dass dieselbe Instanz (nur einmal ausgeführt) mehrere Threads erzeugt, die diese einzelne Codeinstanz verarbeiten. Dies bedeutet, dass mehr als ein sequentieller Steuerungsfluss durch denselben Speicherblock läuft.

Was verstehen wir unter threadsicher ? Wenn mehrere Threads eine einzelne Instanz eines Programms ausführen und daher den Speicher gemeinsam nutzen, versuchen möglicherweise mehrere Threads, an derselben Stelle im Speicher zu lesen und zu schreiben. Schauen wir uns ein Beispiel an. Wenn wir ein Multithread-Programm haben, werden mehrere Threads dieselbe Instanz verarbeiten (siehe Abbildung 1).

Was passiert, wenn Thread-AVariablen untersucht werden instanceVar? Beachten Sie, wie Thread-Bgerade erhöht wurde instanceVar. Das Problem hier wurde in Thread-Adas geschrieben instanceVarund erwartet nicht, dass sich dieser Wert ändert, es sei denn, dies wird Thread-Aausdrücklich getan. Leider Thread-Bdenkt das Gleiche über sich selbst; Das einzige Problem ist, dass sie dieselbe Variable verwenden. Dieses Problem betrifft nicht nur Servlets. Es ist ein häufiges Programmierproblem, das nur beim Multithreading einer Anwendung auftritt. Sie denken wahrscheinlich; "Nun, ich habe nicht nach Multithreading gefragt. Ich möchte nur ein Servlet!" Und ein Servlet ist was du hast. Lassen Sie mich Ihnen unseren Freund den Servlet-Container vorstellen.

Ihr Servlet-Container ist kein Dummy

Zwischen der HTTP-Anfrage des Webbrowsers und dem Code, den wir in die Methoden doGet()und schreiben, geschieht viel Magie doPost(). Der Servlet-Container macht diese "Magie" möglich. Wie jedes Java-Programm muss das Servlet in einer JVM ausgeführt werden, aber für eine Webanwendung haben wir auch die Komplexität der Verarbeitung von HTTP-Anforderungen - hier kommt der Servlet-Container ins Spiel. Der Servlet-Container ist für die Erstellung, Zerstörung und Ausführung Ihrer Servlets verantwortlich. und Ausführung; Die Abfolge dieser Ereignisse wird als Servlet-Lebenszyklus bezeichnet.

Der Lebenszyklus des Servlets ist ein wichtiges Thema und wird daher in der Java-Zertifizierungsprüfung von Sun aufgeführt. Der Grund für seine Bedeutung liegt hauptsächlich darin, dass ein Großteil des Lebenszyklus des Servlets außerhalb der Kontrolle des Programmierers liegt. Wir machen uns (größtenteils) keine großen Sorgen darüber, wie viele Instanzen unseres Servlets zur Laufzeit vorhanden sind. Wir sind auch nicht generell besorgt über die Speichernutzung in Bezug auf die Erstellung und Zerstörung unserer Servlets. Der Grund für unsere mangelnde Besorgnis ist, dass der Servlet-Container dies für uns erledigt (ja, mehr Magie).

Der Servlet-Container verwaltet nicht nur den Lebenszyklus des Servlets, sondern leistet auch gute Arbeit. Der Servlet-Container ist besorgt über die Effizienz. Es stellt sicher, dass Servlets beim Erstellen effizient genutzt werden, und ja, Sie haben es erraten, dies schließt Multithreading ein. Wie Abbildung 2 zeigt, verarbeiten mehrere Threads gleichzeitig Ihr Servlet.

Stellen Sie sich die Leistungsprobleme vor, die bei einer so beliebten Website wie Google oder Amazon auftreten würden, wenn die Websites nicht mit effizienter Multithread-Verarbeitung erstellt würden. Obwohl unsere Webanwendung wahrscheinlich nicht ganz so beliebt ist wie Google, wäre es dennoch nicht praktikabel, eine Website zu erstellen, für die für jede Anforderung ein Servlet instanziiert werden muss. Aus diesem Grund bin ich dankbar, dass der Servlet-Container das Multithreading für mich übernimmt. Andernfalls müssten die meisten von uns die Art und Weise ändern, wie wir unsere Servlets entworfen haben.

Nachdem wir mit den Gefahren von Multithread-Anwendungen vertraut sind und wissen, dass alle unsere Servlets Multithread-Anwendungen sind, schauen wir uns genau an, wann Multithreading für uns ein Problem darstellt.

Bist du threadsicher?

Unten finden Sie ein einfaches Servlet, das nicht threadsicher ist. Schauen Sie genau hin, denn auf den ersten Blick scheint nichts falsch daran zu sein:

Paket threadSafety; import java.io.IOException; import javax.servlet. *; import javax.servlet.http. *; importiere java.math. *; public class SimpleServlet erweitert HttpServlet {// Eine Variable, die NICHT threadsicher ist! privater int Zähler = 0; public void doGet (HttpServletRequest req, HttpServletResponse resp) löst ServletException, IOException {doPost (req, resp) aus; } public void doPost (HttpServletRequest req, HttpServletResponse resp) löst ServletException, IOException {resp.getWriter (). println ("") aus; resp.getWriter (). println (this + ":

"); für (int c = 0; c <10; c ++) {resp.getWriter (). println (" Counter = "+ counter +"

"); try {Thread.currentThread (). sleep ((long) Math.random () * 1000); counter ++;} catch (InterruptedException exc) {}} resp.getWriter (). println (" ");}}

Die Variable counterist eine Instanzvariable, die so genannt wird, weil sie an die Klasseninstanz gebunden ist. Da es in der Klassendefinition definiert ist, gehört es zu dieser Klasseninstanz. Es ist praktisch, unsere Variablen in diesem Bereich zu platzieren, da sie außerhalb der Methoden der Klasse liegen und jederzeit verfügbar sind. Der Wert bleibt auch zwischen Methodenaufrufen erhalten. Das Problem hierbei ist, dass unser Servlet-Container Multithread-fähig ist und einzelne Instanzen von Servlets für mehrere Anforderungen gemeinsam nutzt. Klingt es jetzt nach einer guten Idee, Ihre Variablen als Instanzvariablen zu definieren? Denken Sie daran, dass dieser Variablen nur eine Stelle im Speicher zugewiesen ist und dass sie von allen Threads gemeinsam genutzt wird, die dieselbe Klasseninstanz ausführen möchten.

Lassen Sie uns herausfinden, was passiert, wenn wir dieses Servlet gleichzeitig ausführen. Mit der sleep()Methode fügen wir eine Verzögerung bei der Verarbeitung hinzu . Diese Methode hilft, ein genaueres Verhalten zu simulieren, da sich die meisten Anforderungen in der für die Verarbeitung erforderlichen Zeit unterscheiden. Wie unser Glück als Programmierer ist, tritt unser Problem natürlich auch häufiger auf. Dieses einfache Servlet wird so erhöht counter, dass jedes Servlet sequentielle Werte anzeigen kann. Wir erstellen simultane Anfragen mithilfe von HTML-Frames. Die Quelle jedes Frames ist das gleiche Servlet:

   

Unser Code, ein nicht threadsicheres Servlet, generiert die folgende Ausgabe:

[email protected]: Zähler = 0 Zähler = 2 Zähler = 4 Zähler = 6 Zähler = 9 Zähler = 11 Zähler = 13 Zähler = 15 Zähler = 17 Zähler = 19 [email protected]: Zähler = 0 Zähler = 1 Zähler = 3 Zähler = 5 Zähler = 7 Zähler = 8 Zähler = 10 Zähler = 12 Zähler = 14 Zähler = 16 [email protected]: Zähler = 18 Zähler = 20 Zähler = 22 Zähler = 23 Zähler = 24 Zähler = 25 Zähler = 26 Zähler = 27 Zähler = 28 Zähler = 29 

Wie wir in unserer Ausgabe sehen können, erzielen wir nicht die gewünschten Ergebnisse. Beachten Sie, dass der aus der thisReferenz gedruckte Wert doppelt vorhanden ist. Dies ist die Speicheradresse des Servlets. Es wird uns mitgeteilt, dass nur ein Servlet instanziiert wird, um alle Anforderungen zu bearbeiten. Das Servlet versuchte sein Bestes, um sequentielle Daten auszugeben, aber weil alle Threads den zugewiesenen Speicher gemeinsam nutzencounterWir haben es geschafft, auf unsere eigenen Zehen zu treten. Wir können sehen, dass die Werte nicht immer sequentiell sind, was schlecht ist! Was ist, wenn diese Variable verwendet wird, um auf die privaten Informationen eines Benutzers zu verweisen? Was passiert, wenn sich ein Benutzer bei seinem Online-Banking-System anmeldet und auf einer bestimmten Seite die Bankinformationen eines anderen Benutzers sieht? Dieses Problem kann sich auf viele Arten manifestieren, von denen die meisten schwer zu identifizieren sind. Die gute Nachricht ist jedoch, dass dieses Problem leicht behoben werden kann. Schauen wir uns also unsere Optionen an.

Ihre erste Verteidigung: Vermeidung

Ich habe immer gesagt, dass der beste Weg, um Probleme zu beheben, darin besteht, sie alle zusammen zu vermeiden. In unserem Fall ist dieser Ansatz am besten. Bei der Erörterung der Thread-Sicherheit interessieren uns nur die Variablen, in die wir sowohl lesen als auch schreiben und die sich auf eine bestimmte Webkonversation beziehen. Wenn die Variable schreibgeschützt oder anwendungsweit ist, führt dies nicht dazu, dass dieser Speicherplatz für alle Instanzen freigegeben wird. Für alle anderen Variablenverwendungen möchten wir sicherstellen, dass wir entweder synchronisierten Zugriff auf die Variable haben (mehr dazu gleich) oder dass wir für jeden Thread eine eindeutige Variable haben.

Um sicherzustellen, dass wir für jeden Thread eine eigene eindeutige Variableninstanz haben, verschieben wir einfach die Deklaration der Variablen aus der Klasse in die Methode, die sie verwendet. Wir haben jetzt unsere Variable von einer Instanzvariablen in eine lokale Variable geändert. Der Unterschied besteht darin, dass für jeden Aufruf der Methode eine neue Variable erstellt wird. Daher hat jeder Thread eine eigene Variable. Früher, als die Variable eine Instanzvariable war, wurde die Variable für alle Threads freigegeben, die diese Klasseninstanz verarbeiten. Der folgende thread-sichere Code weist einen subtilen, aber wichtigen Unterschied auf. Beachten Sie, wo die Zählervariable deklariert ist!

import java.io.IOException; import javax.servlet.*; import javax.servlet.http.*; import java.math.*; public class SimpleServlet extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doPost(req, resp); } public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //A variable that IS thread-safe! private int counter = 0; resp.getWriter().println(""); resp.getWriter().println(this + ":

"); for (int c = 0; c < 10; c++) { resp.getWriter().println("Counter = " + counter + "

"); try { Thread.currentThread().sleep((long) Math.random() * 1000); counter++; } catch (InterruptedException exc) { } } resp.getWriter().println(""); } }

Move the variable declaration to within the doGet() method and test again. Notice a change in behavior? I know, you're thinking; "It can't be that easy," but usually it is. As you scramble to revisit your latest servlet code to check where you declared your variables, you may run into a small snag. As you move your variables from within the class definition to within the method, you may find that you were leveraging the scope of the variable and accessing it from within other methods. If you find yourself in this situation, you have a couple of choices. First, change the method interfaces and pass this variable (and any other shared variables) to each method requiring it. I highly recommend this approach. Explicitly passing your data elements from method to method is always best; it clarifies your intentions, documents each method's requirements, makes your code well structured, and offers many other benefits.

If you discover that you must share a variable between servlets and this variable is going to be read from and written to by multiple threads (and you are not storing it in a database), then you will require thread synchronization. Sorry, there is no way around it now.

Your second defense: Partial synchronization

Thread synchronization is an important technique to know, but not one you want to throw at a solution unless required. Anytime you synchronize blocks of code, you introduce bottlenecks into your system. When you synchronize a code block, you tell the JVM that only one thread may be within this synchronized block of code at a given moment. If we run a multithreaded application and a thread runs into a synchronized code block being executed by another thread, the second thread must wait until the first thread exits that block.

Es ist wichtig, genau zu identifizieren, welcher Codeblock wirklich synchronisiert werden muss, und so wenig wie möglich zu synchronisieren. In unserem Beispiel nehmen wir an, dass es keine Option ist, unsere Instanzvariable zu einer lokalen Variablen zu machen. Schauen Sie sich an, wie wir den entscheidenden Codeblock synchronisieren würden: