Unabhängig davon, ob Sie folgen oder nicht, verwenden Java -Webanwendungen Threadpools, um Anforderungen in größerem oder geringem Maße zu verarbeiten. Die Implementierungsdetails von Thread -Pools können ignoriert werden, es ist jedoch erforderlich, früher oder später die Verwendung und Stimmung von Threadpools zu verstehen. In diesem Artikel wird hauptsächlich die Verwendung des Java -Thread -Pools und der korrekten Konfiguration des Thread -Pools vorgestellt.
Einzelfaden
Beginnen wir mit den Grundlagen. Unabhängig davon, welcher Anwendungsserver oder Framework (z. B. Tomcat, Jetty usw.) verwendet wird, haben sie ähnliche grundlegende Implementierungen. Die Grundlage eines Webdienstes ist ein Socket, der für das Anhören des Ports, das Warten auf die TCP -Verbindung und das Akzeptieren der TCP -Verbindung verantwortlich ist. Sobald die TCP -Verbindung akzeptiert wurde, können Daten aus der neu erstellten TCP -Verbindung gelesen und gesendet werden.
Um den oben genannten Prozess zu verstehen, verwenden wir keinen Anwendungsserver direkt, sondern erstellen einen einfachen Webdienst von Grund auf neu. Dieser Dienst ist ein Mikrokosmos der meisten Anwendungsserver. Ein einfacher Webdienst mit einem Thread sieht folgendermaßen aus:
ServerSocket Listener = new ServerSocket (8080); try {while (true) {Socket Socket = listener.accept (); Versuchen Sie {Handlrequest (Socket); } catch (ioException e) {e.printstacktrace (); }}} endlich {listener.close ();}Der obige Code erstellt einen Server -Socket (ServerSocket) , hört auf Port 8080 und schleifen dann den Socket an, um festzustellen, ob eine neue Verbindung besteht. Sobald eine neue Verbindung angenommen wurde, wird der Sockel in die Handlernmethode übergeben. Diese Methode analysiert den Datenstrom in eine HTTP -Anforderung, antwortet und schreibt die Antwortdaten. In diesem einfachen Beispiel implementiert die Handlernmethode lediglich das Lesen des Datenstroms und gibt einfache Antwortdaten zurück. Im Allgemeinen Implementierungen wird diese Methode viel komplexer sein, z. B. das Lesen von Daten aus einer Datenbank usw.
endgültige statische String-Antwort = "HTTP/1.0 200 OK/R/N" + "Inhaltstyp: Text/Plain/R/N" + "/r/n" + "Hallo Welt/r/n"; public static void Handlreequest (Socket Socket) löscht IOException (// den Eingabestream lesen und return "200 OK" try {bufferedReader in = new bufferedReader (neuer InputStreamReader (Socket.getInputStream ()); log.info (in.readline ()); OutputStream out = socket.getOutputStream (); out.write (response.getBytes (StandardCharets.utf_8)); } endlich {socket.close (); }}Da es nur einen Thread gibt, der die Anfrage bearbeitet, muss jede Anfrage warten, bis die vorherige Anfrage bearbeitet wird, bevor sie beantwortet werden kann. Unter der Annahme, dass eine Anfrage -Antwortzeit 100 Millisekunden beträgt, beträgt die Anzahl der Antworten pro Sekunde (TPS) dieses Servers nur 10.
Multi-Threaded
Obwohl die Handlernmethode auf IO blockiert werden kann, kann die CPU weiterhin weitere Anfragen bearbeiten. In einem einzigen Fall kann dies jedoch nicht getan werden. Daher kann die parallele Verarbeitungsfunktion des Servers durch Erstellen von Multi-Threading-Methoden verbessert werden.
öffentliche statische Klassenhandlers -Implements Runnable {Final Socket Socket; public Handlreenquestrunnable (Socket Socket) {this.socket = Socket; } public void run () {try {HandleRequest (Socket); } catch (ioException e) {e.printstacktrace (); }}} Serversocket louser = new ServerSocket (8080); try {while (true) {Socket Socket = listener.accept (); neuer Thread (neuer Handlehrer (Socket)). start (); }} endlich {listener.close ();}Hier wird die Methode Accept () im Haupt -Thread weiterhin aufgerufen, aber sobald die TCP -Verbindung hergestellt wurde, wird ein neuer Thread erstellt, um die neue Anforderung zu verarbeiten, bei der die Handlernmethode im vorherigen Text im neuen Thread ausgeführt wird.
Durch das Erstellen eines neuen Threads kann der Haupt -Thread weiterhin neue TCP -Verbindungen akzeptieren, und diese Anfragen können parallel bearbeitet werden. Diese Methode wird als "ein Thread pro Anforderung" bezeichnet. Natürlich gibt es andere Möglichkeiten, die Verarbeitungsleistung zu verbessern, z.
In jeder Anfrage ist eine Thread -Implementierung sehr teuer, einen Thread (und eine anschließende Zerstörung) zu erstellen, da sowohl das JVM als auch das Betriebssystem Ressourcen zuweisen müssen. Darüber hinaus hat die obige Implementierung auch ein Problem, dh die Anzahl der erstellten Threads ist unkontrollierbar, was dazu führen kann, dass die Systemressourcen schnell erschöpft werden.
Erschöpfte Ressourcen
Jeder Thread erfordert eine bestimmte Menge an Stapelspeicherraum. In der letzten 64-Bit-JVM beträgt die Standardstapelgröße 1024 KB. Wenn der Server eine große Anzahl von Anforderungen empfängt oder die Handlernmethode langsam ausgeführt wird, kann der Server aufgrund des Erstellens einer großen Anzahl von Threads abstürzen. Beispielsweise gibt es 1000 parallele Anfragen, und die 1000 erstellten Threads müssen 1 GB JVM -Speicher als Thread -Stapel -Speicherplatz verwenden. Darüber hinaus können Objekte, die während der Ausführung des Codes jedes Threads erstellt wurden, auch auf dem Haufen erstellt werden. Wenn sich diese Situation verschlechtert, überschreitet sie den JVM -Heap -Speicher und erzeugen eine große Menge an Müllsammlungsvorgängen, die schließlich den Speicherüberlauf (OutofMemoryErrors) verursachen.
Diese Threads verbrauchen nicht nur Speicher, sondern verwenden auch andere begrenzte Ressourcen, z. B. Dateihandles, Datenbankverbindungen usw. Unkontrollierbare Erstellungs -Threads können auch andere Arten von Fehlern und Abstürze verursachen. Ein wichtiger Weg, um die Erschöpfung der Ressourcen zu vermeiden, besteht daher darin, unkontrollierbare Datenstrukturen zu vermeiden.
Übrigens kann die Stapelgröße aufgrund von Speicherproblemen, die durch die Stapelgröße verursacht werden, über den -xSS -Schalter eingestellt werden. Nachdem die Stapelgröße des Fadens reduziert wird, kann der Overhead pro Faden reduziert werden, aber der Stapelüberlauf (Stackoverflowerrors) kann erhöht werden. Für allgemeine Anwendungen ist der Standard 1024KB zu reichhaltig und es ist möglicherweise angemessener, ihn auf 256 KB oder 512 KB zu reduzieren. Der minimal zulässige Wert in Java beträgt 160 KB.
Fadenpool
Um eine kontinuierliche Erstellung neuer Fäden zu vermeiden, können Sie die Obergrenze des Fadenpools mithilfe eines einfachen Fadenpools einschränken. Der Thread -Pool verwaltet alle Threads. Wenn die Anzahl der Fäden die Obergrenze nicht erreicht hat, erstellt der Fadenpool Fäden an der Obergrenze und wiederverwendet freie Fäden wie möglich.
ServerSocket Listener = new ServerSocket (8080); ExecutorService Executor = Executors.NewFixedThreadpool (4); try {while (true) {Socket Socket = louser.accept (); Executor. }} endlich {listener.close ();}In diesem Beispiel wird der Executorservice anstatt den Thread direkt zu erstellen. Es übernimmt die Aufgaben, die ausgeführt werden müssen (um die Runnables -Schnittstelle implementieren zu müssen) in den Thread -Pool und führt den Code mithilfe von Threads im Thread -Pool aus. Im Beispiel wird ein Threadpool mit fester Größe mit einer Reihe von Threads von 4 verwendet, um alle Anforderungen zu verarbeiten. Dadurch wird die Anzahl der Threads eingeschränkt, die Anfragen verarbeiten und auch die Verwendung von Ressourcen einschränken.
Neben der NewfixedThreadpool-Methode erstellen Sie nicht nur einen Threadpool mit fester Größe, sondern bietet auch die Ausführungsklasse auch die NewCachedThreadpool-Methode. Die Wiederverwendung eines Thread -Pools kann weiterhin zu einer unkontrollierbaren Anzahl von Threads führen, aber die Leerlauffäden, die so weit wie möglich erstellt wurden. Normalerweise eignet sich diese Art von Threadpool für kurze Aufgaben, die nicht durch externe Ressourcen blockiert werden.
Arbeitswarteschlange
Was passieren, wenn alle Threads ein Fixed-Green-Thread-Pool verwendet haben, wenn alle Threads gefüllt sind, wenn eine andere Anfrage kommt? ThreadPoolexecutor verwendet eine Warteschlange, um anhängige Anforderungen zu halten, und Threadpools mit fester Größe verwenden standardmäßig unbegrenzte verknüpfte Listen. Beachten Sie, dass dies wiederum zu Problemen mit Ressourcenerschöpfung führen kann, aber es wird nicht geschehen, solange die Fadenverarbeitungsgeschwindigkeit größer ist als die Wachstumsrate der Warteschlange. Im vorherigen Beispiel enthält jede Anforderung in der Warteschlange einen Socket, der in einigen Betriebssystemen das Dateihandle verbraucht. Da das Betriebssystem die Anzahl der nach dem Prozess geöffneten Dateiverhandlungen einschränkt, ist es am besten, die Größe der Arbeitswarteschlange zu begrenzen.
public static ExecutorService NeubundFixedThreadpool (int nthreads, int capacity) {return neuer threadpoolexecutor (Nthreads, Nthreads, 0L, TimeUnit.Milliseconds, New LinkedBlocking BlockingCleue <Runnable> (Kapazität), neue Threadpoolexecutor. löst ioException {ServerSocket listener = new serversocket (8080); ExecutorService Executor = newBoundFixed threadpool (4, 16); try {while (true) {Socket Socket = listener.accept (); Executor. }} endlich {listener.close (); }}Anstatt direkt mit den ausführenden Ausführern zu verwenden.
Wenn alle Themen beschäftigt sind, wird die neue Aufgabe in die Warteschlange gefüllt. Da die Warteschlange die Größe auf 16 Elemente begrenzt, muss diese Grenze beim Erstellen des ThreadPoolexecutor -Objekts durch den letzten Parameter behandelt werden. In dem Beispiel wird eine Verwirrung verwendet, dh wenn die Warteschlange die Obergrenze erreicht, wird die neue Aufgabe verworfen. Zusätzlich zum ersten Mal gibt es eine Abgebichtsrichtlinie (AbortPolicy) und eine Caller -Ausführungsrichtlinie (Callerrunspolicy). Ersteres wird eine Ausnahme auswerfen, während letzteres die Aufgabe im Anrufer -Thread ausführen wird.
Bei Webanwendungen sollte die optimale Standardrichtlinie bestehen, um die Richtlinie aufzugeben oder abzubrechen und einen Fehler an den Client zurückzugeben (z. B. einen HTTP 503 -Fehler). Natürlich ist es auch möglich, Kundenanfragen aufzugeben, indem die Länge der Arbeitswarteschlange erhöht wird. Benutzeranfragen sind jedoch im Allgemeinen nicht bereit, lange zu warten, und dies verbraucht mehr Serverressourcen. Der Zweck der Arbeitswarteschlange besteht nicht darin, auf Kundenanfragen ohne Grenzen zu reagieren, sondern um reibungslose und platze Anfragen zu veranlassen. Normalerweise sollte die Arbeitswarteschlange leer sein.
Tuning der Fadenzahl
Das vorherige Beispiel zeigt, wie ein Thread -Pool erstellt und verwendet wird. Das Kernproblem bei der Verwendung eines Thread -Pools ist jedoch, wie viele Threads verwendet werden sollten. Zunächst müssen wir sicherstellen, dass die Ressource nicht erschöpft ist, wenn die Fadenlimit erreicht ist. Zu den Ressourcen hier gehören Speicher (Heap und Stack), Anzahl der geöffneten Dateigriffe, die Anzahl der TCP -Verbindungen, die Anzahl der Remote -Datenbankverbindungen und andere begrenzte Ressourcen. Insbesondere, wenn Tätigkeitsaufgaben rechenintensiv sind, ist die Anzahl der CPU -Kerne auch eine der Ressourcenbeschränkungen. Im Allgemeinen sollte die Anzahl der Threads die Anzahl der CPU -Kerne nicht überschreiten.
Da die Auswahl der Thread -Anzahl von der Art der Anwendung abhängt, kann eine Menge Leistungstests erforderlich sein, bevor die optimalen Ergebnisse erzielt werden können. Natürlich können Sie auch die Leistung Ihrer Anwendung verbessern, indem Sie die Anzahl der Ressourcen erhöhen. Ändern Sie beispielsweise die JVM Heap -Speichergröße oder ändern Sie die obere Grenze des Dateigriffs des Betriebssystems usw. Dann treffen diese Anpassungen schließlich auf die theoretische Obergrenze.
Little's Gesetz
Little's Law beschreibt die Beziehung zwischen drei Variablen in einem stabilen System.
Wenn L die durchschnittliche Anzahl von Anfragen darstellt, stellt λ die Häufigkeit von Anfragen und w die durchschnittliche Zeit für die Beantwortung der Anfrage dar. Wenn beispielsweise die Anzahl der Anfragen pro Sekunde 10 beträgt und jede Anforderungsverarbeitungszeit 1 Sekunde beträgt, werden zu jedem Zeitpunkt 10 Anfragen bearbeitet. Zurück zu unserem Thema müssen 10 Threads verarbeitet werden. Wenn sich die Verarbeitungszeit einer einzelnen Anforderung verdoppelt, verdoppelt sich auch die Anzahl der verarbeiteten Threads und wird 20.
Nach dem Verständnis der Auswirkungen der Verarbeitungszeit auf die Effizienz der Anfrage werden wir feststellen, dass die theoretische Obergrenze möglicherweise nicht der optimale Wert für die Gewindepoolgröße ist. Die Obergrenze des Thread Pools erfordert auch eine Referenzaufgabenverarbeitungszeit.
Unter der Annahme, dass die JVM 1000 Aufgaben parallel bearbeiten kann, können im schlimmsten Fall höchstens 33,3 Anfragen pro Sekunde bearbeitet werden, wenn jede Anforderungsverarbeitungszeit nicht über 30 Sekunden überschreitet. Wenn jedoch jede Anfrage nur 500 Millisekunden dauert, kann die Anwendung 2000 Anforderungen pro Sekunde bearbeiten.
Split -Thread -Pool
In Microservices oder serviceorientierten Architekturen (SOA) ist in der Regel Zugang zu mehreren Backend-Diensten erforderlich. Wenn einer der Dienste verschlechtert wird, kann der Thread -Pool keine Threads ausführen, was die Anforderungen an andere Dienste beeinflusst.
Ein effektiver Weg, um mit dem Backend -Service -Fehler umzugehen, besteht darin, den von jedem Dienst verwendeten Thread -Pool zu isolieren. In diesem Modus gibt es immer noch einen versandten Thread -Pool, der Aufgaben in verschiedene Backend -Anforderungs -Thread -Pools sendet. Dieser Thread -Pool hat möglicherweise aufgrund eines langsamen Backends keine Last und überträgt die Belastung in einen Thread -Pool, der langsames Backend anfordert.
Darüber hinaus muss der Pooling-Modus mit Multi-Threaded auch Deadlock-Probleme vermeiden. Wenn jeder Thread blockiert, während er auf das Ergebnis einer unverarbeiteten Anfrage wartet, tritt ein Deadlock auf. Im Multithread -Poolmodus muss daher die von jedem Thread -Pool ausgeführten Aufgaben und die Abhängigkeiten zwischen ihnen verstehen, um Deadlockprobleme so weit wie möglich zu vermeiden.
Zusammenfassen
Auch wenn Threadpools nicht direkt in der Anwendung verwendet werden, werden sie wahrscheinlich indirekt vom Anwendungsserver oder Framework in der Anwendung verwendet. Frameworks wie Tomcat, Jboss, Totow, DropWizard usw. bieten alle Optionen zum Einstellen von Thread -Pools (Thread -Pools, die von Servlet Execution verwendet werden).
Ich hoffe, dieser Artikel kann Ihr Verständnis des Thread -Pools verbessern und Ihnen beim Lernen helfen.