Java 101: Java-Parallelität ohne Schmerzen, Teil 1

Angesichts der zunehmenden Komplexität gleichzeitiger Anwendungen stellen viele Entwickler fest, dass die Threading-Funktionen von Java auf niedriger Ebene nicht für ihre Programmieranforderungen geeignet sind. In diesem Fall ist es möglicherweise an der Zeit, die Java Concurrency Utilities zu ermitteln. Beginnen Sie java.util.concurrentmit Jeff Friesens detaillierter Einführung in das Executor-Framework, die Synchronisierertypen und das Java Concurrent Collections-Paket.

Java 101: Die nächste Generation

Der erste Artikel in dieser neuen JavaWorld-Reihe stellt die Java-API für Datum und Uhrzeit vor .

Die Java-Plattform bietet Threading-Funktionen auf niedriger Ebene, mit denen Entwickler gleichzeitig Anwendungen schreiben können, in denen verschiedene Threads gleichzeitig ausgeführt werden. Standard-Java-Threading hat jedoch einige Nachteile:

  • Java Low-Level - Parallelität Primitiven ( synchronized, volatile, wait(), notify(), und notifyAll()) ist nicht einfach zu benutzen zu können. Threading-Gefahren wie Deadlock, Thread-Hunger und Race-Bedingungen, die sich aus der falschen Verwendung von Grundelementen ergeben, sind ebenfalls schwer zu erkennen und zu debuggen.
  • Die Verwendung synchronizedzur Koordinierung des Zugriffs zwischen Threads führt zu Leistungsproblemen, die sich auf die Skalierbarkeit von Anwendungen auswirken. Dies ist eine Anforderung für viele moderne Anwendungen.
  • Die grundlegenden Threading-Funktionen von Java sind zu niedrig. Entwickler benötigen häufig übergeordnete Konstrukte wie Semaphoren und Thread-Pools, die die Low-Level-Threading-Funktionen von Java nicht bieten. Infolgedessen erstellen Entwickler ihre eigenen Konstrukte, was sowohl zeitaufwändig als auch fehleranfällig ist.

Das Framework JSR 166: Concurrency Utilities wurde entwickelt, um die Notwendigkeit einer Threading-Funktion auf hoher Ebene zu erfüllen. Das Anfang 2002 initiierte Framework wurde zwei Jahre später in Java 5 formalisiert und implementiert. In Java 6, Java 7 und dem kommenden Java 8 wurden Verbesserungen vorgenommen.

Diese zweiteilige Serie Java 101: Die nächste Generation führt Softwareentwickler ein, die mit dem grundlegenden Java-Threading in den Paketen und dem Framework der Java Concurrency Utilities vertraut sind. In Teil 1 präsentiere ich einen Überblick über das Java Concurrency Utilities-Framework und stelle das Executor-Framework, die Synchronizer-Dienstprogramme und das Java Concurrent Collections-Paket vor.

Grundlegendes zu Java-Threads

Bevor Sie in diese Serie eintauchen, stellen Sie sicher, dass Sie mit den Grundlagen des Einfädelns vertraut sind. Beginnen Sie mit der Einführung von Java 101 in die Low-Level-Threading-Funktionen von Java:

  • Teil 1: Einführung in Threads und Runnables
  • Teil 2: Thread-Synchronisation
  • Teil 3: Thread-Planung, Warten / Benachrichtigen und Thread-Unterbrechung
  • Teil 4: Thread-Gruppen, Volatilität, Thread-lokale Variablen, Timer und Thread-Tod

Innerhalb der Java Concurrency Utilities

Das Java Concurrency Utilities-Framework ist eine Bibliothek von Typen , die als Bausteine ​​zum Erstellen gleichzeitiger Klassen oder Anwendungen verwendet werden sollen. Diese Typen sind threadsicher, wurden gründlich getestet und bieten eine hohe Leistung.

Typen in den Java Concurrency Utilities sind in kleinen Frameworks organisiert. nämlich Executor Framework, Synchronizer, gleichzeitige Sammlungen, Sperren, atomare Variablen und Fork / Join. Sie sind weiter in ein Hauptpaket und ein Paar Unterpakete unterteilt:

  • java.util.concurrent enthält allgemeine Dienstprogrammtypen, die häufig bei der gleichzeitigen Programmierung verwendet werden. Beispiele hierfür sind Semaphoren, Barrieren, Thread-Pools und gleichzeitige Hashmaps.
    • Das Unterpaket java.util.concurrent.atomic enthält Dienstprogrammklassen auf niedriger Ebene, die eine sperrenfreie threadsichere Programmierung für einzelne Variablen unterstützen.
    • Das Unterpaket java.util.concurrent.locks enthält Dienstprogramme auf niedriger Ebene zum Sperren und Warten auf Bedingungen, die sich von der Verwendung von Java-Synchronisierung und -Monitoren auf niedriger Ebene unterscheiden.

Das Java Concurrency Utilities-Framework stellt auch die Hardware-Anweisung zum Vergleichen und Austauschen auf niedriger Ebene (CAS) bereit, deren Varianten üblicherweise von modernen Prozessoren unterstützt werden. CAS ist viel leichter als der Monitor-basierte Synchronisationsmechanismus von Java und wird zum Implementieren einiger hoch skalierbarer gleichzeitiger Klassen verwendet. Die CAS-basierte java.util.concurrent.locks.ReentrantLockKlasse ist beispielsweise leistungsfähiger als das entsprechende Monitor-basierte synchronizedGrundelement. ReentrantLockbietet mehr Kontrolle über die Verriegelung. (In Teil 2 werde ich mehr darüber erklären, wie CAS funktioniert java.util.concurrent.)

System.nanoTime ()

Das Java Concurrency Utilities-Framework enthält long nanoTime()ein Mitglied der java.lang.SystemKlasse. Diese Methode ermöglicht den Zugriff auf eine Zeitquelle mit Nanosekunden-Granularität für relative Zeitmessungen.

In den nächsten Abschnitten werde ich drei nützliche Funktionen der Java Concurrency Utilities vorstellen. Zunächst wird erläutert, warum sie für die moderne Parallelität so wichtig sind, und anschließend wird gezeigt, wie sie die Geschwindigkeit, Zuverlässigkeit, Effizienz und Skalierbarkeit gleichzeitiger Java-Anwendungen erhöhen.

Das Executor-Framework

Beim Einfädeln ist eine Aufgabe eine Arbeitseinheit. Ein Problem beim Low-Level-Threading in Java besteht darin, dass die Übermittlung von Aufgaben eng mit einer Richtlinie zur Ausführung von Aufgaben verbunden ist, wie in Listing 1 gezeigt.

Listing 1. Server.java (Version 1)

import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; class Server { public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(9000); while (true) { final Socket s = socket.accept(); Runnable r = new Runnable() { @Override public void run() { doWork(s); } }; new Thread(r).start(); } } static void doWork(Socket s) { } }

Der obige Code beschreibt eine einfache Serveranwendung (der doWork(Socket)Kürze halber leer gelassen). Der Server-Thread ruft wiederholt socket.accept()auf, um auf eine eingehende Anforderung zu warten, und startet dann einen Thread, um diese Anforderung zu bearbeiten, wenn sie eintrifft.

Da diese Anwendung für jede Anforderung einen neuen Thread erstellt, lässt sie sich bei einer großen Anzahl von Anforderungen nicht gut skalieren. Beispielsweise benötigt jeder erstellte Thread Speicher, und zu viele Threads können den verfügbaren Speicher erschöpfen und die Anwendung zum Beenden zwingen.

Sie können dieses Problem lösen, indem Sie die Richtlinie zur Ausführung von Aufgaben ändern. Anstatt immer einen neuen Thread zu erstellen, können Sie einen Thread-Pool verwenden, in dem eine feste Anzahl von Threads eingehende Aufgaben bearbeitet. Sie müssten die Anwendung jedoch neu schreiben, um diese Änderung vorzunehmen.

java.util.concurrentEnthält das Executor-Framework, ein kleines Framework von Typen, die die Übermittlung von Aufgaben von Richtlinien für die Ausführung von Aufgaben entkoppeln. Mit dem Executor-Framework können Sie die Taskausführungsrichtlinie eines Programms einfach optimieren, ohne Ihren Code erheblich umschreiben zu müssen.

Innerhalb des Executor-Frameworks

Das Executor-Framework basiert auf der ExecutorSchnittstelle, die einen Executor als jedes Objekt beschreibt, das java.lang.RunnableAufgaben ausführen kann. Diese Schnittstelle deklariert die folgende Einzelmethode zum Ausführen einer RunnableAufgabe:

void execute(Runnable command)

Sie senden eine RunnableAufgabe, indem Sie sie an übergeben execute(Runnable). Wenn der Executor die Aufgabe aus irgendeinem Grund nicht ausführen kann (z. B. wenn der Executor heruntergefahren wurde), löst diese Methode a aus RejectedExecutionException.

Das Schlüsselkonzept besteht darin, dass die Aufgabenübermittlung von der Aufgabenausführungsrichtlinie entkoppelt ist, die durch eine ExecutorImplementierung beschrieben wird . Die ausführbare Task kann somit über einen neuen Thread, einen gepoolten Thread, den aufrufenden Thread usw. ausgeführt werden.

Beachten Sie, dass dies Executorsehr begrenzt ist. Sie können beispielsweise keinen Executor herunterfahren oder feststellen, ob eine asynchrone Aufgabe abgeschlossen wurde. Sie können eine laufende Aufgabe auch nicht abbrechen. Aus diesen und anderen Gründen bietet das Executor-Framework eine ExecutorService-Schnittstelle, die erweitert wird Executor.

Fünf der ExecutorServiceMethoden sind besonders hervorzuheben:

  • boolean awaitTermination (lange Zeitüberschreitung, TimeUnit-Einheit) blockiert den aufrufenden Thread, bis alle Aufgaben nach einer Anforderung zum Herunterfahren ausgeführt wurden, die Zeitüberschreitung auftritt oder der aktuelle Thread unterbrochen wird, je nachdem, was zuerst eintritt. Die maximale Wartezeit wird durch angegeben timeout, und dieser Wert wird in den unitdurch die TimeUnitAufzählung angegebenen Einheiten ausgedrückt . zum Beispiel TimeUnit.SECONDS. Diese Methode wird ausgelöst, java.lang.InterruptedExceptionwenn der aktuelle Thread unterbrochen wird. Es gibt true zurück , wenn der Executor beendet wird, und false, wenn das Timeout vor dem Beenden abgelaufen ist.
  • boolean isShutdown () gibt true zurück , wenn der Executor heruntergefahren wurde.
  • void shutdown () initiiert ein ordnungsgemäßes Herunterfahren, bei dem zuvor übermittelte Aufgaben ausgeführt werden, aber keine neuen Aufgaben akzeptiert werden.
  • Zukünftige Übermittlung (aufrufbare Aufgabe) übermittelt eine wertrückgebende Aufgabe zur Ausführung und gibt eine FutureDarstellung der ausstehenden Ergebnisse der Aufgabe zurück.
  • Future Submit (ausführbare Aufgabe) sendet eine RunnableAufgabe zur Ausführung und gibt eine FutureDarstellung dieser Aufgabe zurück.

Die FutureSchnittstelle repräsentiert das Ergebnis einer asynchronen Berechnung. Das Ergebnis wird als Zukunft bezeichnet, da es normalerweise erst zu einem späteren Zeitpunkt verfügbar sein wird. Sie können Methoden aufrufen, um eine Aufgabe abzubrechen, das Ergebnis einer Aufgabe zurückzugeben (auf unbestimmte Zeit warten oder auf eine Zeitüberschreitung, wenn die Aufgabe noch nicht abgeschlossen ist) und feststellen, ob eine Aufgabe abgebrochen oder beendet wurde.

Die CallableSchnittstelle ähnelt der RunnableSchnittstelle darin, dass sie eine einzelne Methode bereitstellt, die eine auszuführende Aufgabe beschreibt. Im Gegensatz Runnablezur void run()Methode kann Callabledie V call() throws ExceptionMethode einen Wert zurückgeben und eine Ausnahme auslösen.

Executor Factory-Methoden

Irgendwann möchten Sie einen Executor erhalten. Das Executor-Framework stellt Executorszu diesem Zweck die Utility-Klasse bereit. Executorsbietet verschiedene Factory-Methoden zum Abrufen verschiedener Arten von Executoren, die spezifische Richtlinien für die Thread-Ausführung bieten. Hier sind drei Beispiele:

  • ExecutorService newCachedThreadPool () erstellt einen Thread-Pool, der bei Bedarf neue Threads erstellt, zuvor erstellte Threads jedoch wiederverwendet, wenn sie verfügbar sind. Threads, die 60 Sekunden lang nicht verwendet wurden, werden beendet und aus dem Cache entfernt. Dieser Thread-Pool verbessert normalerweise die Leistung von Programmen, die viele kurzlebige asynchrone Aufgaben ausführen.
  • ExecutorService newSingleThreadExecutor () erstellt einen Executor, der einen einzelnen Worker-Thread verwendet, der in einer unbegrenzten Warteschlange ausgeführt wird. Aufgaben werden der Warteschlange hinzugefügt und nacheinander ausgeführt (es ist nicht mehr als eine Aufgabe gleichzeitig aktiv). Wenn dieser Thread während der Ausführung vor dem Herunterfahren des Executors durch einen Fehler beendet wird, wird ein neuer Thread erstellt, der seinen Platz einnimmt, wenn nachfolgende Aufgaben ausgeführt werden müssen.
  • ExecutorService newFixedThreadPool (int nThreads) erstellt einen Thread-Pool, der eine feste Anzahl von Threads wiederverwendet, die in einer gemeinsam genutzten unbegrenzten Warteschlange ausgeführt werden. Die meisten nThreadsThreads verarbeiten aktiv Aufgaben. Wenn zusätzliche Aufgaben gesendet werden, während alle Threads aktiv sind, warten sie in der Warteschlange, bis ein Thread verfügbar ist. Wenn ein Thread während der Ausführung vor dem Herunterfahren durch einen Fehler beendet wird, wird ein neuer Thread erstellt, der seinen Platz einnimmt, wenn nachfolgende Aufgaben ausgeführt werden müssen. Die Threads des Pools sind vorhanden, bis der Executor heruntergefahren wird.

Der Rahmen Executor bietet zusätzliche Typen (wie die ScheduledExecutorServiceSchnittstelle), aber die Typen , die Sie wahrscheinlich an der Arbeit sind mit am häufigsten sind ExecutorService, Future, Callable, und Executors.

Weitere java.util.concurrentInformationen zu verschiedenen Typen finden Sie im Javadoc.

Arbeiten mit dem Executor-Framework

Sie werden feststellen, dass das Executor-Framework ziemlich einfach zu handhaben ist. In Listing 2 habe ich verwendet , Executorund Executorsden Server Beispiel aus Listing 1 mit einer skalierbarer Thread - Pool-basierten Alternative zu ersetzen.

Listing 2. Server.java (Version 2)

import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.Executor; import java.util.concurrent.Executors; class Server { static Executor pool = Executors.newFixedThreadPool(5); public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(9000); while (true) { final Socket s = socket.accept(); Runnable r = new Runnable() { @Override public void run() { doWork(s); } }; pool.execute(r); } } static void doWork(Socket s) { } }

Listing 2 verwendet newFixedThreadPool(int), um einen Thread-Pool-basierten Executor zu erhalten, der fünf Threads wiederverwendet. Es ersetzt auch new Thread(r).start();mit pool.execute(r);für runnable Aufgaben über eine dieser Threads ausgeführt wird .

Listing 3 zeigt ein weiteres Beispiel, in dem eine Anwendung den Inhalt einer beliebigen Webseite liest. Es gibt die resultierenden Zeilen oder eine Fehlermeldung aus, wenn der Inhalt nicht innerhalb von maximal fünf Sekunden verfügbar ist.