Heim  >  Artikel  >  Java  >  Eine kurze Diskussion zum Vergleich mehrerer häufig verwendeter Thread-Pools in Java

Eine kurze Diskussion zum Vergleich mehrerer häufig verwendeter Thread-Pools in Java

高洛峰
高洛峰Original
2017-01-23 16:15:191262Durchsuche

1. Warum Thread-Pools verwenden

Viele Serveranwendungen wie Webserver, Datenbankserver, Dateiserver oder Mailserver sind darauf ausgerichtet, eine große Anzahl kurzer Aufgaben von einer Remote-Quelle aus zu verarbeiten. Die Anfrage erreicht den Server auf irgendeine Weise, vielleicht über ein Netzwerkprotokoll (wie HTTP, FTP oder POP), über eine JMS-Warteschlange oder vielleicht durch Abfragen einer Datenbank. Unabhängig davon, wie die Anfragen eintreffen, kommt es bei Serveranwendungen häufig vor, dass die Bearbeitungszeit einer einzelnen Aufgabe sehr kurz ist, die Anzahl der Anfragen jedoch enorm ist.

Ein einfaches Modell zum Erstellen von Serveranwendungen besteht darin, jedes Mal, wenn eine Anfrage eintrifft, einen neuen Thread zu erstellen und die Anfrage dann im neuen Thread zu bedienen. Dieser Ansatz eignet sich eigentlich gut für die Prototypenerstellung. Wenn Sie jedoch versuchen, eine Serveranwendung bereitzustellen, die auf diese Weise ausgeführt wird, werden die gravierenden Mängel dieses Ansatzes deutlich. Einer der Nachteile des Thread-pro-Anfrage-Ansatzes besteht darin, dass das Erstellen eines neuen Threads für jede Anfrage teuer ist. Der Server, der für jede Anfrage einen neuen Thread erstellt, verbringt viel Zeit damit, Threads zu erstellen und zu zerstören Ressourcen, als für die Bearbeitung tatsächlicher Benutzeranfragen aufgewendet werden.

Zusätzlich zum Aufwand für das Erstellen und Zerstören von Threads verbrauchen aktive Threads auch Systemressourcen. Das Erstellen zu vieler Threads in einer JVM kann dazu führen, dass dem System aufgrund übermäßigen Speicherverbrauchs der Speicher ausgeht oder es zu einem „Overswitch“ kommt. Um einen Ressourcenmangel zu verhindern, benötigen Serveranwendungen eine Möglichkeit, die Anzahl der Anfragen zu begrenzen, die sie zu einem bestimmten Zeitpunkt verarbeiten können.

Thread-Pools bieten Lösungen für Thread-Lebenszyklus-Overhead-Probleme und Ressourcenknappheitsprobleme. Durch die Wiederverwendung von Threads für mehrere Aufgaben wird der Aufwand für die Thread-Erstellung auf mehrere Aufgaben verteilt. Der Vorteil besteht darin, dass die durch die Thread-Erstellung verursachte Verzögerung auch unbeabsichtigt eliminiert wird, da der Thread bereits vorhanden ist, wenn die Anforderung eintrifft. Auf diese Weise können Anfragen sofort bearbeitet werden, wodurch die Anwendung schneller reagiert. Darüber hinaus werden durch entsprechende Anpassung der Anzahl der Threads im Thread-Pool, d. h. wenn die Anzahl der Anforderungen einen bestimmten Schwellenwert überschreitet, alle anderen eingehenden Anforderungen gezwungen, zu warten, bis ein Thread für ihre Verarbeitung verfügbar ist, wodurch Ressourcenengpässe vermieden werden.

2. Risiken bei der Verwendung von Thread-Pools

Thread-Pools sind zwar ein leistungsstarker Mechanismus zum Erstellen von Multithread-Anwendungen, ihre Verwendung ist jedoch nicht ohne Risiken. Mit Thread-Pools erstellte Anwendungen sind anfällig für alle Parallelitätsrisiken, denen jede andere Multithread-Anwendung ausgesetzt ist, z. B. Synchronisierungsfehler und Deadlocks. Sie sind auch anfällig für einige andere Risiken, die für Thread-Pools spezifisch sind, z. B. poolbezogene Deadlocks , Unzureichende Ressourcen und Thread-Lecks.

2.1 Deadlock

Bei jeder Multithread-Anwendung besteht das Risiko eines Deadlocks. Wenn jeder Prozess oder Thread einer Gruppe auf ein Ereignis wartet, das nur von einem anderen Prozess in der Gruppe verursacht werden kann, spricht man von einem Deadlock der Gruppe von Prozessen oder Threads. Der einfachste Fall eines Deadlocks ist: Thread A hält eine exklusive Sperre für Objekt X und wartet auf eine Sperre für Objekt Y, während Thread B eine exklusive Sperre für Objekt Y hält, aber auf eine Sperre für Objekt X wartet. Sofern es keine Möglichkeit gibt, das Warten auf die Sperre zu unterbrechen (Java-Sperrung unterstützt diese Methode nicht), wird der blockierte Thread ewig warten.

Während in jedem Multithread-Programm das Risiko eines Deadlocks besteht, führen Thread-Pools zu einer weiteren Möglichkeit eines Deadlocks, bei dem alle Pool-Threads in einer blockierten Warteschlange ausgeführt werden. Eine Aufgabe, die das Ergebnis der Ausführung von ist eine andere Aufgabe, aber diese Aufgabe kann nicht ausgeführt werden, da keine unbesetzten Threads vorhanden sind. Dies geschieht, wenn ein Thread-Pool verwendet wird, um eine Simulation mit vielen interagierenden Objekten zu implementieren. Die simulierten Objekte können sich gegenseitig Abfragen senden, und diese Abfragen werden dann als Aufgaben in der Warteschlange ausgeführt, während die Abfrageobjekte synchron auf Antworten warten.

2.2 Unzureichende Ressourcen

Ein Vorteil von Thread-Pools besteht darin, dass sie im Vergleich zu anderen alternativen Planungsmechanismen (von denen wir einige bereits besprochen haben) im Allgemeinen eine sehr gute Leistung erbringen. Dies gilt jedoch nur, wenn die Thread-Pool-Größe entsprechend angepasst wird. Threads verbrauchen viele Ressourcen, einschließlich Speicher und andere Systemressourcen. Zusätzlich zum vom Thread-Objekt benötigten Speicher benötigt jeder Thread zwei Ausführungsaufrufstapel, die groß sein können. Darüber hinaus erstellt die JVM möglicherweise einen nativen Thread für jeden Java-Thread, und diese nativen Threads verbrauchen zusätzliche Systemressourcen. Obwohl der Planungsaufwand für den Wechsel zwischen Threads gering ist, kann der Kontextwechsel bei vielen Threads die Leistung des Programms erheblich beeinträchtigen.

Wenn der Thread-Pool zu groß ist, können die von diesen Threads verbrauchten Ressourcen die Systemleistung ernsthaft beeinträchtigen. Das Wechseln zwischen Threads verschwendet Zeit, und die Verwendung von mehr Threads, als Sie tatsächlich benötigen, kann zu Ressourcenmangel führen, da die Pool-Threads Ressourcen verbrauchen, die von anderen Aufgaben möglicherweise effizienter genutzt werden könnten. Zusätzlich zu den vom Thread selbst verwendeten Ressourcen sind für die Bearbeitung der Anforderung möglicherweise weitere Ressourcen erforderlich, z. B. JDBC-Verbindungen, Sockets oder Dateien. Auch hier handelt es sich um begrenzte Ressourcen, und zu viele gleichzeitige Anforderungen können zu Fehlern führen, z. B. weil keine JDBC-Verbindung zugewiesen werden kann.

2.3 Parallelitätsfehler

Thread-Pools und andere Warteschlangenmechanismen basieren auf der Verwendung der Methoden wait() und notify(), die beide schwierig zu verwenden sind. Bei falscher Codierung können Benachrichtigungen verloren gehen, was dazu führt, dass der Thread im Leerlauf bleibt, obwohl in der Warteschlange noch Arbeit zur Verarbeitung vorhanden ist. Bei der Anwendung dieser Methoden ist äußerste Vorsicht geboten. Stattdessen ist es besser, eine vorhandene Implementierung zu verwenden, von der bereits bekannt ist, dass sie funktioniert, beispielsweise das Paket util.concurrent.

2.4 Thread-Leck

Ein ernstes Risiko bei verschiedenen Arten von Thread-Pools ist Thread-Leck, wenn ein Thread aus dem Pool entfernt wird, um eine Aufgabe auszuführen, und nachdem die Aufgabe abgeschlossen ist, wird der Thread gelöscht Dies geschieht, wenn der Pool nicht zurückgegeben wird. Eine Situation, in der Thread-Lecks auftreten, ist, wenn eine Aufgabe eine RuntimeException oder einen Fehler auslöst. Wenn die Pool-Klasse sie nicht abfängt, wird der Thread einfach beendet und die Größe des Thread-Pools wird dauerhaft um eins reduziert. Wenn dies häufig genug geschieht, leert sich der Thread-Pool schließlich und das System bleibt stehen, da keine Threads zur Bewältigung der Aufgabe verfügbar sind.

Einige Aufgaben warten möglicherweise ewig auf bestimmte Ressourcen oder Eingaben des Benutzers, und es kann nicht garantiert werden, dass diese Ressourcen verfügbar werden. Der Benutzer ist möglicherweise nach Hause gegangen, und solche Aufgaben werden dauerhaft gestoppt, und diese gestoppten Aufgaben können verursachen auch die gleichen Probleme wie Thread-Lecks. Wenn ein Thread dauerhaft von einer solchen Aufgabe beansprucht wird, wird er effektiv aus dem Pool entfernt. Für solche Aufgaben sollte man ihnen entweder nur einen eigenen Thread geben oder sie nur eine begrenzte Zeit warten lassen.

2.5 Anforderungsüberlastung

Es ist möglich, den Server mit nur Anforderungen zu überlasten. In diesem Szenario möchten wir möglicherweise nicht jede eingehende Anforderung in unsere Arbeitswarteschlange einreihen, da zur Ausführung in die Warteschlange gestellte Aufgaben möglicherweise zu viele Systemressourcen verbrauchen und zu Ressourcenknappheit führen. Was Sie in dieser Situation tun, bleibt Ihnen überlassen. In einigen Fällen können Sie die Anfrage einfach abbrechen und sich darauf verlassen, dass die Anfrage später von einem übergeordneten Protokoll erneut ausgeführt wird, oder Sie können mit einer Antwort antworten, die angibt, dass der Server vorübergehend nicht verfügbar ist beschäftigt, die Anfrage abzulehnen.

3. Richtlinien für die effektive Nutzung von Thread-Pools

Thread-Pools können eine äußerst effektive Möglichkeit sein, Serveranwendungen zu erstellen, solange Sie ein paar einfache Richtlinien befolgen:

Don' t Aufgaben in eine Warteschlange stellen, die synchron auf die Ergebnisse anderer Aufgaben warten. Dies kann zu der oben beschriebenen Form eines Deadlocks führen, bei dem alle Threads mit Aufgaben belegt sind, die wiederum auf die Ergebnisse von Aufgaben in der Warteschlange warten, die nicht ausgeführt werden können, da alle Threads sehr beschäftigt sind.

Seien Sie vorsichtig, wenn Sie gepoolte Threads für potenziell lange Vorgänge verwenden. Wenn das Programm auf den Abschluss einer Ressource wie E/A warten muss, geben Sie die maximale Wartezeit an und geben Sie an, ob die Aufgabe anschließend ungültig gemacht oder für eine spätere Ausführung erneut in die Warteschlange gestellt werden soll. Auf diese Weise wird sichergestellt, dass irgendwann Fortschritte erzielt werden, indem ein Thread für eine Aufgabe freigegeben wird, die möglicherweise erfolgreich abgeschlossen wird.

Verstehen Sie die Aufgabe. Um den Thread-Pool effektiv dimensionieren zu können, müssen Sie die Aufgaben verstehen, die in die Warteschlange gestellt werden, und wissen, was sie tun. Sind sie CPU-gebunden? Sind sie I/O-gebunden? Ihre Antwort wird sich darauf auswirken, wie Sie Ihre Bewerbung anpassen. Wenn Sie verschiedene Aufgabenklassen mit sehr unterschiedlichen Eigenschaften haben, kann es sinnvoll sein, mehrere Arbeitswarteschlangen für verschiedene Aufgabenklassen zu haben, damit jeder Pool entsprechend optimiert werden kann.

4. Einstellung der Thread-Pool-Größe

Das Anpassen der Größe des Thread-Pools dient im Wesentlichen dazu, zwei Arten von Fehlern zu vermeiden: zu wenige Threads oder zu viele Threads. Glücklicherweise ist die Spanne zwischen zu viel und zu wenig bei den meisten Anwendungen recht groß.

Zur Erinnerung: Die Verwendung von Threads in einer Anwendung bietet zwei Hauptvorteile: Sie ermöglichen die Fortsetzung der Verarbeitung, obwohl auf langsame Vorgänge wie E/A gewartet wird, und nutzen die Vorteile mehrerer Prozessoren. Bei Anwendungen, die unter den Rechenbeschränkungen einer Maschine mit N Prozessoren ausgeführt werden, kann das Hinzufügen zusätzlicher Threads, wenn sich die Anzahl der Threads N nähert, die Gesamtverarbeitungsleistung verbessern, während das Hinzufügen zusätzlicher Threads, wenn die Anzahl der Threads N übersteigt, keine Auswirkung hat. Tatsächlich können zu viele Threads sogar die Leistung beeinträchtigen, da sie zusätzlichen Aufwand für die Kontextumschaltung verursachen.

Die optimale Größe des Thread-Pools hängt von der Anzahl der verfügbaren Prozessoren und der Art der Aufgaben in der Arbeitswarteschlange ab. Wenn Sie auf einem System mit N Prozessoren, bei denen es sich ausschließlich um Rechenaufgaben handelt, nur eine Arbeitswarteschlange haben, erhalten Sie im Allgemeinen die maximale CPU-Auslastung, wenn der Thread-Pool über N oder N+1 Threads verfügt.

Für Aufgaben, die möglicherweise auf den Abschluss von E/A warten müssen (z. B. Aufgaben, die HTTP-Anforderungen von einem Socket lesen), müssen Sie zulassen, dass die Poolgröße die Anzahl der verfügbaren Prozessoren überschreitet, da nicht alle verfügbar sind Threads funktionieren immer. Mithilfe der Profilerstellung können Sie das Verhältnis von Wartezeit (WT) zu Servicezeit (ST) für eine typische Anfrage schätzen. Wenn wir dieses Verhältnis WT/ST nennen, müssten für ein System mit N Prozessoren ungefähr N*(1+WT/ST) Threads eingerichtet werden, um die Prozessoren voll auszulasten.

Die Prozessorauslastung ist nicht die einzige Überlegung bei der Thread-Pool-Größe. Wenn Ihr Thread-Pool wächst, stoßen Sie möglicherweise auf Einschränkungen beim Scheduler, beim verfügbaren Speicher oder bei anderen Systemressourcen, z. B. der Anzahl der Sockets, offenen Dateihandles oder Datenbankverbindungen.

5. Mehrere häufig verwendete Thread-Pools

5.1 newCachedThreadPool

Erstellen Sie einen zwischenspeicherbaren Thread-Pool. Wenn die Länge des Thread-Pools den Verarbeitungsbedarf übersteigt, können inaktive Threads flexibel recycelt werden . Wenn kein Recycling erfolgt, erstellen Sie einen neuen Thread.

Die Merkmale dieser Art von Thread-Pool sind:

• Es gibt fast keine Begrenzung für die Anzahl der erstellten Arbeitsthreads (tatsächlich gibt es eine Begrenzung, die Anzahl ist Ganzzahl. MAX_VALUE ), sodass der Thread-Pool flexibel hinzugefügt werden kann Thread hinzufügen in .

• Wenn längere Zeit keine Aufgabe an den Thread-Pool übermittelt wird, d. h. wenn der Arbeitsthread für die angegebene Zeit inaktiv ist (Standard ist 1 Minute), wird der Arbeitsthread automatisch beendet. Wenn Sie nach der Beendigung eine neue Aufgabe senden, erstellt der Thread-Pool einen Arbeitsthread neu.

• Bei der Verwendung von CachedThreadPool müssen Sie darauf achten, die Anzahl der Aufgaben zu kontrollieren. Andernfalls kann es aufgrund einer großen Anzahl gleichzeitig laufender Threads zu einer Lähmung des Systems kommen.

Der Beispielcode lautet wie folgt:

package test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExecutorTest {
 public static void main(String[] args) {
 ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
 for (int i = 0; i < 10; i++) {
  final int index = i;
  try {
  Thread.sleep(index * 1000);
  } catch (InterruptedException e) {
  e.printStackTrace();
  }
  cachedThreadPool.execute(new Runnable() {
  public void run() {
   System.out.println(index);
  }
  });
 }
 }
}

5.1 newFixedThreadPool

Erstellen Sie einen Thread-Pool mit einer angegebenen Nummer von Arbeitsthreads. Immer wenn eine Aufgabe übermittelt wird, wird ein Arbeitsthread erstellt. Wenn die Anzahl der Arbeitsthreads die anfängliche maximale Anzahl des Thread-Pools erreicht, wird die übermittelte Aufgabe in der Poolwarteschlange gespeichert.

FixedThreadPool ist ein typischer und hervorragender Thread-Pool. Er bietet die Vorteile eines Thread-Pools, der die Programmeffizienz verbessert und Overhead beim Erstellen von Threads spart. Wenn der Thread-Pool jedoch inaktiv ist, dh wenn sich keine ausführbaren Aufgaben im Thread-Pool befinden, werden die Arbeitsthreads nicht freigegeben und es werden auch bestimmte Systemressourcen belegt.

Der Beispielcode lautet wie folgt:

package test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExecutorTest {
 public static void main(String[] args) {
 ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
 for (int i = 0; i < 10; i++) {
  final int index = i;
  fixedThreadPool.execute(new Runnable() {
  public void run() {
   try {
   System.out.println(index);
   Thread.sleep(2000);
   } catch (InterruptedException e) {
   e.printStackTrace();
   }
  }
  });
 }
 }
}

Da die Thread-Pool-Größe 3 beträgt, schläft jede Aufgabe nach der Ausgabe 2 Sekunden lang Der Index, sodass jede Aufgabe 3 Zahlen in zwei Sekunden ausgibt.

Die Größe des Thread-Pools fester Länge wird am besten entsprechend den Systemressourcen wie Runtime.getRuntime().availableProcessors() festgelegt.

5.1 newSingleThreadExecutor

Erstellen Sie einen Single-Threaded-Executor, dh erstellen Sie nur einen eindeutigen Worker-Thread zum Ausführen von Aufgaben, um sicherzustellen, dass alle Aufgaben ausgeführt werden Folgen Sie der Ausführung in der angegebenen Reihenfolge (FIFO, LIFO, Priorität). Wenn dieser Thread abnormal endet, wird er durch einen anderen ersetzt, um eine sequenzielle Ausführung sicherzustellen. Das größte Merkmal eines einzelnen Arbeitsthreads besteht darin, dass er sicherstellen kann, dass Aufgaben nacheinander ausgeführt werden und nicht mehrere Threads gleichzeitig aktiv sind.

Der Beispielcode lautet wie folgt:

package test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExecutorTest {
 public static void main(String[] args) {
 ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
 for (int i = 0; i < 10; i++) {
  final int index = i;
  singleThreadExecutor.execute(new Runnable() {
  public void run() {
   try {
   System.out.println(index);
   Thread.sleep(2000);
   } catch (InterruptedException e) {
   e.printStackTrace();
   }
  }
  });
 }
 }
}

5.1 newScheduleThreadPool

Erstellt einen Thread-Pool fester Länge und unterstützt das Timing und die periodische Aufgabenausführung und unterstützt die geplante und periodische Aufgabenausführung.

Verzögert die Ausführung um 3 Sekunden. Der Beispielcode für die verzögerte Ausführung lautet wie folgt:

package test;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExecutorTest {
 public static void main(String[] args) {
 ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
 scheduledThreadPool.schedule(new Runnable() {
  public void run() {
  System.out.println("delay 3 seconds");
  }
 }, 3, TimeUnit.SECONDS);
 }
}

bedeutet, dass die Ausführung alle 3 Sekunden nach einer Verzögerung von 1 erfolgt Zweitens lautet der Beispielcode für die reguläre Ausführung wie folgt:

package test;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExecutorTest {
 public static void main(String[] args) {
 ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
 scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
  public void run() {
  System.out.println("delay 1 seconds, and excute every 3 seconds");
  }
 }, 1, 3, TimeUnit.SECONDS);
 }
}

Der obige Artikel, in dem der Vergleich mehrerer häufig verwendeter Thread-Pools in Java kurz erläutert wird, ist der gesamte vom Herausgeber geteilte Inhalt Ich hoffe, dass es Ihnen eine Referenz geben kann, und ich hoffe auch, dass Sie die chinesische PHP-Website unterstützen.

Weitere Artikel zum Vergleich mehrerer in Java häufig verwendeter Thread-Pools finden Sie auf der chinesischen PHP-Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn