So passen Sie den ThreadPoolExecutor-Thread-Pool elegant an

1. Übersicht

Java muss häufig Multithreading verwenden, um einige Geschäfte abzuwickeln. Es wird nicht empfohlen, Threads einfach durch die Vererbung von Thread oder die Implementierung der Runnable-Schnittstelle zu erstellen. Dies führt unweigerlich zu ressourcenintensiven Problemen und Problemen beim Thread-Kontextwechsel beim Erstellen und Zerstören von Threads. Gleichzeitig kann das Erstellen zu vieler Threads auch das Risiko einer Ressourcenerschöpfung mit sich bringen. Zu diesem Zeitpunkt ist es sinnvoller, einen Thread-Pool einzuführen, um die Verwaltung von Thread-Aufgaben zu erleichtern.

Die zugehörigen Klassen, die sich auf den Thread-Pool in Java beziehen, befinden sich alle im Paket java.util.concurrent ab JDK 1.5. Zu den beteiligten Kernklassen und Schnittstellen gehören: Executor, Executors, ExecutorService, ThreadPoolExecutor, FutureTask, Callable, Runnable usw.

In der Executors-Toolklasse sind mehrere Möglichkeiten für JDK zum automatischen Erstellen von Thread-Pools gekapselt:

  • newFixedThreadPool

Der verwendete Konstruktor ist

new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue())

Setzen Sie corePoolSize=maxPoolSize, keepAliveTime=0 (dieser Parameter hat derzeit keine Auswirkung), unbegrenzte Warteschlange, Aufgaben können auf unbestimmte Zeit platziert werden, wenn zu viele Anforderungen vorliegen (die Geschwindigkeit der Aufgabenverarbeitung kann nicht mit der Geschwindigkeit der Aufgabenübermittlung und der Anforderung mithalten). Akkumulation) kann zu einer übermäßigen Belegung des Speichers führen oder direkt eine OOM-Ausnahme verursachen.

  • newSingleThreadExector

Der verwendete Konstruktor ist

new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), var0)

Im Grunde dasselbe wie newFixedThreadPool, aber die Anzahl der Threads ist auf 1 eingestellt, Single-Threaded, und die Nachteile sind die gleichen wie bei newFixedThreadPool.

  • newCachedThreadPool

Der verwendete Konstruktor ist

new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue())

corePoolSize = 0, maxPoolSize ist eine sehr große Zahl, die Warteschlange wird synchron übergeben, dh der residente Thread (Kern-Thread) wird nicht beibehalten und bei jeder Anforderung wird direkt ein neuer Thread zur Verarbeitung erstellt Die Aufgabe und der Warteschlangenpuffer werden nicht verwendet und der Überschuss wird automatisch wiederhergestellt. Da bei Threads maxPoolSize auf Integer.MAX_VALUE gesetzt ist, werden bei vielen Anforderungen möglicherweise zu viele Threads erstellt, was zu einer Ressourcenerschöpfung OOM führt.

  • newScheduledThreadPool

Der verwendete Konstruktor ist

new ThreadPoolExecutor(var1, 2147483647, 0L, TimeUnit.NANOSECONDS, new ScheduledThreadPoolExecutor.DelayedWorkQueue())

Es unterstützt Timing und periodische Ausführung. Beachten Sie, dass die Verzögerungswarteschlange verwendet wird und die Nachteile die gleichen sind wie bei newCachedThreadPool.

Oben wurde also gesagt, dass der mit der Executors-Toolklasse erstellte Thread-Pool versteckte Gefahren birgt. Wie kann man ihn also verwenden, um diese versteckte Gefahr zu vermeiden? Wie nutzt man den Thread-Pool am elegantesten? Wie ist es sinnvoll, einen eigenen Thread-Pool in der Produktionsumgebung zu konfigurieren? Sie müssen das richtige Medikament verschreiben, Ihre eigene Thread-Factory-Klasse erstellen und wichtige Parameter flexibel festlegen.

2. ThreadPoolExecutor

Um den Thread-Pool anzupassen, müssen Sie die ThreadPoolExecutor-Klasse verwenden.

Der Konstruktor der ThreadPoolExecutor-Klasse:

public ThreadPoolExecutor(int coreSize,int maxSize,long KeepAliveTime,TimeUnit unit,BlockingQueue queue,ThreadFactory factory,RejectedExectionHandler handler)

Die obige Konstruktionsmethode hat insgesamt sieben Parameter und die Bedeutung dieser sieben Parameter ist:

  • corePoolSize: Die Anzahl der Kernthreads ist auch die Anzahl der residenten Threads im Thread-Pool. Wenn der Thread-Pool initialisiert wird, gibt es standardmäßig keine Threads. Wenn die Aufgabe kommt, wird der Thread erstellt, um die Aufgabe auszuführen.
  • MaximumPoolSize: Die maximale Anzahl von Threads. Einige Nicht-Kern-Threads können basierend auf der Anzahl der Kern-Threads hinzugefügt werden. Es ist zu beachten, dass nur dann mehr Threads als CorePoolSize erstellt werden, wenn die WorkQueue-Warteschlange voll ist (die Gesamtzahl). Threads im Thread-Pool überschreiten nicht maxPoolSize )
  • keepAliveTime: Nicht zum Kern gehörende Threads werden automatisch beendet und recycelt, wenn die Leerlaufzeit keepAliveTime überschreitet. Beachten Sie, dass bei corePoolSize = maxPoolSize der Parameter keepAliveTime nicht funktioniert (da es keine nicht zum Kern gehörenden Threads gibt);
  • Einheit: die Zeiteinheit von keepAliveTime
  • workQueue: Die zum Speichern von Aufgaben verwendete Warteschlange, bei der es sich um einen von drei Warteschlangentypen handeln kann: unbegrenzte, begrenzte und synchrone Übergabe. Wenn die Anzahl der Arbeitsthreads im Pool größer als corePoolSize ist, werden neue eingehende Aufgaben in die Warteschlange gestellt
  • threadFactory: Eine Factory-Klasse zum Erstellen von Threads, die standardmäßig Executors.defaultThreadFactory() verwendet oder zum Erstellen den ThreadFactoryBuilder der Guava-Bibliothek verwendet
  • Handler: Die Sättigungsrichtlinie, wenn der Thread-Pool keine Aufgaben mehr empfangen kann (die Warteschlange ist voll und die Anzahl der Threads erreicht die maximale PoolSize). Die Werte sind AbortPolicy, CallerRunsPolicy, DiscardOldestPolicy, DiscardPolicy

3. Bezogen auf die Thread-Pool-Konfiguration

3.1 Einstellung der Thread-Pool-Größe

Um dieses Problem zu lösen, müssen wir zunächst klären, ob unsere Anforderungen rechenintensiv oder IO-intensiv sind. Nur wenn wir dies verstehen, können wir die Anzahl der Thread-Pools besser begrenzen.

  1. Rechenintensiv

Wie der Name schon sagt, benötigt die Anwendung viele CPU-Rechenressourcen. Im Zeitalter der Multi-Core-CPUs müssen wir jedem CPU-Kern erlauben, an der Berechnung teilzunehmen und die Leistung der CPU voll auszunutzen. Auf diese Weise wird die Die Serverkonfiguration wird nicht verschwendet. Wenn die Serverkonfiguration sehr gut ist. Was für eine große Verschwendung wäre es, ein Single-Thread-Programm auf dem Computer auszuführen. Bei rechenintensiven Anwendungen hängt die Arbeit vollständig von der Anzahl der CPU-Kerne ab. Um die Vorteile voll auszunutzen und übermäßigen Thread-Kontextwechsel zu vermeiden, lautet die ideale Lösung:

Die Anzahl der Threads = die Anzahl der CPU-Kerne + 1 oder sie kann auf die Anzahl der CPU-Kerne * 2 festgelegt werden, hängt aber auch von der JDK-Version und der CPU-Konfiguration ab (die CPU des Servers verfügt über Hyperthreading).

Stellen Sie im Allgemeinen CPU * 2 ein.

  1. I/O-intensiv

Der größte Teil der Entwicklung, die wir derzeit durchführen, sind WEB-Anwendungen, die viel Netzwerkübertragung erfordern. Darüber hinaus umfasst die Interaktion mit der Datenbank und dem Cache auch E/A. Sobald E/A auftritt, befindet sich der Thread in einem Wartezustand. Wann Die E/A ist beendet. Der Thread wird nicht weiter ausgeführt, bis die Daten bereit sind. Daher können wir hier feststellen, dass wir für E/A-intensive Anwendungen mehr Threads im Thread-Pool festlegen können, sodass die Threads während der Wartezeit auf E/A andere Aufgaben erledigen und die Effizienz der gleichzeitigen Verarbeitung verbessern können. Kann die Datenmenge in diesem Thread-Pool also beliebig eingestellt werden? Natürlich nicht. Bitte denken Sie daran, dass der Thread-Kontextwechsel seinen Preis hat. Derzeit ist eine Reihe von Formeln für IO-intensive Anwendungen zusammengefasst:

Anzahl der Threads = Anzahl der CPU-Kerne/(1-Blockierungskoeffizient) Dieser Blockierungskoeffizient liegt im Allgemeinen zwischen 0,8 und 0,9 und kann auch 0,8 oder 0,9 betragen.

Bei Anwendung der Formel beträgt die ideale Anzahl an Threads für eine Dual-Core-CPU 20. Dies ist natürlich nicht absolut und muss entsprechend der tatsächlichen Situation und dem tatsächlichen Geschäft angepasst werden: final int poolSize = (int)(cpuCore/ (1-0,9) )

Bezüglich des Blockierungskoeffizienten gibt es einen Satz, der in „Programming Concurrency on the JVM Mastering“ oder „Java Virtual Machine Concurrent Programming“ erwähnt wird:

Für den Blockierungsfaktor können wir zunächst versuchen, ihn zu erraten, oder einige empfindliche Analysetools oder Java verwenden.

3.2 Thread-Pool-bezogene Parameterkonfiguration

  • Achten Sie darauf, keine Konfigurationselemente auszuwählen, für die es keine Obergrenze gibt.

Aus diesem Grund wird nicht empfohlen, die Methode zum Erstellen von Threads in Executors zu verwenden.

Beispielsweise können die Einstellungen von Executors.newCachedThreadPool und die Einstellung unbegrenzter Warteschlangen aufgrund einiger unvorhersehbarer Situationen zu Systemausnahmen im Thread-Pool führen, was zu einem Anstieg der Threads oder einer kontinuierlichen Erweiterung der Aufgabenwarteschlange sowie zu Systemabstürzen und Ausnahmen führt Gedächtniserschöpfung.

Um dieses Problem zu vermeiden, wird empfohlen, einen benutzerdefinierten Thread-Pool zu verwenden. Dies ist auch das erste Prinzip bei der Verwendung der Thread-Pool-Spezifikation!

  • Zweitens legen Sie die Anzahl der Threads und die Wiederherstellungszeit im Thread-Leerlauf angemessen fest.

Stellen Sie es entsprechend dem spezifischen Ausführungszyklus und der Zeit der Aufgabe ein, um häufiges Recycling und Erstellen zu vermeiden. Obwohl der Zweck der Verwendung des Thread-Pools darin besteht, die Systemleistung und den Durchsatz zu verbessern, müssen wir auch die Stabilität des Systems berücksichtigen, da sonst unvorhersehbare Probleme auftreten sehr mühsam sein!

  • Drittens wählen Sie basierend auf dem tatsächlichen Szenario eine Ablehnungsstrategie, die auf Sie zutrifft.

Machen Sie eine Entschädigung, machen Sie sich nicht mit dem von JDK unterstützten automatischen Kompensationsmechanismus an! Versuchen Sie, eine individuelle Ablehnungsstrategie zu verwenden, um das Endergebnis abzudecken!

  • Viertens kann die Thread-Pool-Ablehnungsstrategie und die benutzerdefinierte Ablehnungsstrategie die RejectedExecutionHandler-Schnittstelle implementieren.

Die mit dem JDK gelieferte Ablehnungsstrategie lautet wie folgt:

AbortPolicy: Das Auslösen einer Ausnahme verhindert direkt, dass das System normal funktioniert.
CallerRunsPolicy: Solange der Thread-Pool nicht geschlossen ist, führt diese Richtlinie die aktuell verworfene Aufgabe direkt im Aufrufer-Thread aus.
DiscardOldestPolicy: Verwerfen Sie die älteste Anfrage und versuchen Sie erneut, die aktuelle Aufgabe zu senden.
DiscardPolicy: Verwerfen Sie Aufgaben, die ohne Verarbeitung nicht verarbeitet werden können.

4. Verwenden von Hooks

Verwenden Sie Hook, um die Ausführungsspur des Thread-Pools zu verlassen:

ThreadPoolExecutor bietet eine Hook-Methode, mit der der geschützte Typ überschrieben werden kann, sodass Benutzer etwas tun können, bevor die Aufgabe ausgeführt wird. Wir können damit Vorgänge wie das Initialisieren von ThreadLocal, das Sammeln von Statistiken und das Aufzeichnen von Protokollen implementieren. Solche Hooks sind beforeExecute und afterExecute. Es gibt auch einen Hook, mit dem Benutzer Logik einfügen können, wenn die Aufgabe ausgeführt wird, z. B. erneute Terminierung.

Wenn die Hook-Methode fehlschlägt, schlägt die Ausführung des internen Worker-Threads fehl oder wird unterbrochen.

Wir können beforeExecute und afterExecute verwenden, um einige Ausführungsbedingungen vor und nach dem Thread aufzuzeichnen oder den Status nach Abschluss der Ausführung direkt in einem Protokollsystem wie ELK aufzuzeichnen.

5. Schließen Sie den Thread-Pool

Wenn auf den Thread-Pool nicht mehr verwiesen wird und die Anzahl der Arbeitsthreads 0 beträgt, wird der Thread-Pool beendet. Wir können auch Shutdown aufrufen, um den Thread-Pool manuell zu beenden. Wenn wir vergessen, Shutdown aufzurufen, können wir zur Freigabe der Thread-Ressourcen auch keepAliveTime undallowCoreThreadTimeOut verwenden, um das Ziel zu erreichen!

Der sichere Weg besteht natürlich darin, die Methode Runtime.getRuntime().addShutdownHook der virtuellen Maschine zu verwenden, um die Shutdown-Methode des Thread-Pools manuell aufzurufen.

6. Elemente, die optimiert werden können

6.1 Legen Sie den Thread im Thread-Pool als Daemon fest

Unter normalen Umständen beendet der Thread-Pool nach dem Schließen des Thread-Pools automatisch die darin enthaltenen Threads. Bei einigen Threads, die vorgeben, sie selbst oder direkt new Thread() zu sein, wird der Prozess jedoch weiterhin daran gehindert, geschlossen zu werden.

Gemäß der Beurteilungsmethode zum Herunterfahren von Java-Prozessen wird der Prozess normal geschlossen, wenn nur Daemon-Threads vorhanden sind. Daher wird empfohlen, diese Nicht-Hauptthreads als Daemons festzulegen, das heißt, sie blockieren nicht das Schließen des Prozesses.

6.2 Korrekt benannter Thread

Bei Verwendung des Thread-Pools wird im Allgemeinen das ThreadFactory-Objekt verwendet, um zu steuern, wie Threads erstellt werden. Wenn dieser Parameter im mit Java gelieferten ExecutorService nicht festgelegt ist, wird die Standardeinstellung DefaultThreadFactory verwendet. Der Effekt ist, dass Sie in der Thread-Stack-Liste eine Reihe von Pool-x-Thread-y sehen. Wenn Sie jstack tatsächlich verwenden, können Sie die Gruppe, zu der jeder dieser Threads gehört, und die spezifische Funktion nicht sehen.

6.3 Verwerfen Sie periodische Aufgaben, die nicht mehr verfügbar sind

Im Allgemeinen wird bei Verwendung des in Java enthaltenen ScheduledThreadPoolExecutor durch Aufrufen von ScheduleAtFixedRate und ScheduleWithFixedDelay die Aufgabe auf periodisch (Zeitraum) festgelegt. Wenn der Thread-Pool geschlossen wird, können diese Aufgaben direkt verworfen werden (standardmäßig). Wenn Sie jedoch den Zeitplan verwenden, um Langzeitaufgaben hinzuzufügen, schließt der Thread-Pool den entsprechenden Thread nicht, da es sich nicht um eine periodische Aufgabe handelt

Beispielsweise wird TriggerTask (einschließlich CronTask) im Federsystem zum Timing von Planungsaufgaben verwendet, die letztendlich über den Zeitplan geplant werden. Nachdem eine einzelne Aufgabe abgeschlossen ist, wird die nächste Aufgabe erneut über den Zeitplan ausgeführt. Diese Methode wird nicht als Zeitraum betrachtet. Daher wird bei Verwendung dieser Planungsmethode die Shutdown-Methode zwar ausgeführt, wenn der Container geschlossen wird, der entsprechende zugrunde liegende ScheduledExecutorService wird jedoch immer noch nicht erfolgreich geschlossen (obwohl alle Status festgelegt wurden). Der Endeffekt besteht darin, dass Sie einen Thread-Pool sehen, der sich bereits im Shutdown-Status befindet, der Thread jedoch noch ausgeführt wird (der Status ist eine Warteaufgabe).

Um diese Methode zu lösen, stellt Java einen zusätzlichen Einstellungsparameter „executeExistingDelayedTasksAfterShutdown“ bereit. Dieser Wert ist standardmäßig „true“, dh er wird nach dem Herunterfahren weiterhin ausgeführt. Sie können ihn beim Definieren des Thread-Pools auf „false“ setzen, d. h. nach dem Schließen des Thread-Pools werden diese verzögerten Aufgaben nicht mehr ausgeführt.

Supongo que te gusta

Origin blog.csdn.net/qq_43842093/article/details/131262789
Recomendado
Clasificación