Sequentielle Programmierung , das heißt, alles im Programm kann jeweils nur einen Schritt ausführen. Gleichzeitige Programmierung , das Programm kann mehrere Teile des Programms parallel ausführen.
Threads können Aufgaben steuern, daher benötigen Sie eine Möglichkeit, Aufgaben zu beschreiben, die von der Runnable
-Schnittstelle bereitgestellt werden kann. Um eine Aufgabe zu definieren, implementieren Sie einfach die Runnable
-Schnittstelle und schreiben Sie die run()
-Methode, damit die Aufgabe Ihre Befehle ausführen kann.
Wenn eine Klasse von Runnable
abgeleitet wird, muss sie eine run()
-Methode haben, aber diese Methode hat nichts Besonderes – sie bietet keine inhärenten Threading-Funktionen. Um Thread-Verhalten zu implementieren, müssen Sie eine Aufgabe explizit an einen Thread anhängen.
FixedThreadPool
und CachedThreadPool
FixedThreadPool
kann eine kostspielige Thread-Zuweisung auf einmal vorab ausgeführt werden. Daher kann die Anzahl der Threads begrenzt werden. Dies spart Zeit, da Sie nicht den festen Aufwand für die Erstellung eines Threads für jede Aufgabe bezahlen müssen. In ereignisgesteuerten Systemen können Event-Handler, die Threads benötigen, auch nach Wunsch bedient werden, indem die Threads direkt aus dem Pool bezogen werden. Sie werden die verfügbaren Ressourcen nicht missbrauchen, da die Anzahl der von FixedThreadPool verwendeten Thread-Objekte begrenzt ist.
Beachten Sie, dass in jedem Thread-Pool vorhandene Threads nach Möglichkeit automatisch wiederverwendet werden.
Obwohl in diesem Buch CachedThreadPool
verwendet wird, sollten Sie auch die Verwendung von FiexedThreadPool
in Code in Betracht ziehen, der Threads erzeugt. CachedThreadPool
Erstellt normalerweise während der Programmausführung so viele Threads wie nötig und stoppt dann die Erstellung neuer Threads, wenn alte Threads recycelt werden. Daher ist dies eine vernünftige Executor
erste Wahl. Nur wenn diese Methode Probleme bereitet, müssen Sie auf FixedThreadPool
umsteigen.
SingleThreadExecutor
ist wie 1
mit der Anzahl der Threads FixedThreadPool
. (Es bietet auch eine wichtige Parallelitätsgarantie, dass kein anderer Thread (d. h. keine zwei Threads) aufgerufen wird. Dadurch ändern sich die Sperranforderungen der Aufgabe.)
Wenn mehrere Aufgaben an SingleThreadExecutor
gesendet werden, werden die Aufgaben ausgeführt In der Warteschlange wird jede Aufgabe bis zum Ende ausgeführt, bevor die nächste Aufgabe beginnt, und alle Aufgaben verwenden denselben Thread. Im folgenden Beispiel können Sie sehen, dass jede Aufgabe in der Reihenfolge abgeschlossen wird, in der sie übermittelt wurde, und bevor die nächste Aufgabe beginnt. Daher serialisiert SingleThreadExecutor
alle an ihn übermittelten Aufgaben und verwaltet eine eigene (versteckte) Warteschlange ausstehender Aufgaben.
Runnable
ist eine unabhängige Aufgabe, die Arbeit ausführt, aber keinen Aufgabenwert zurückgibt. Wenn Sie möchten, dass die Aufgabe nach Abschluss einen Wert zurückgibt, können Sie die Callable
-Schnittstelle anstelle der Runnable
-Schnittstelle implementieren. Der in Java SE5 eingeführte Callable
ist ein generischer Typ mit einem Typparameter. Sein Typparameter stellt den von der Methode call()
zurückgegebenen Wert dar (anstelle von run()
), und die Methode ExecutorService.submit()
muss verwendet werden.
Eine weitere Redewendung, die Sie möglicherweise sehen, ist das selbstverwaltete Runnable
.
Dies unterscheidet sich nicht besonders vom Erben von Thread
, außer dass die Syntax etwas unklarer ist. Durch die Implementierung einer Schnittstelle können Sie jedoch von einer anderen Klasse erben, während dies beim Erben von Thread
nicht der Fall ist.
Beachten Sie, dass das selbstverwaltete Runnable
im Konstruktor aufgerufen wird. Dieses Beispiel ist ziemlich einfach und daher wahrscheinlich sicher, aber Sie sollten sich darüber im Klaren sein, dass das Starten eines Threads in einem Konstruktor problematisch werden kann, da möglicherweise eine andere Aufgabe mit der Ausführung beginnt, bevor der Konstruktor endet, was bedeutet, dass die Aufgabe in einem instabilen Zustand auf Objekte zugreifen kann. Dies ist ein weiterer Grund, Executor
der expliziten Erstellung eines Thread对
-Objekts vorzuziehen.
Thread-Gruppe enthält eine Sammlung von Threads. Der Wert von Thread-Gruppen lässt sich mit einem Zitat von Joshua Bloch zusammenfassen: „Am besten stellt man sich Thread-Gruppen als einen erfolglosen Versuch vor, den man einfach ignorieren kann.“
Wenn Sie (wie ich) viel Zeit und Mühe darauf verwendet haben, den Wert von Thread-Gruppen herauszufinden, dann werden Sie vielleicht überrascht sein, warum es keine offizielle Stellungnahme von Sun gibt zu diesem Thema seit Jahren. Die gleiche Frage wurde seitdem unzählige Male zu anderen Änderungen in Java gestellt. Die Lebensphilosophie von Joseph Stiglitz, dem Nobelpreisträger für Wirtschaftswissenschaften, kann zur Erklärung dieses Problems herangezogen werden: „Die Kosten für das fortgesetzte Begehen von Fehlern werden von anderen getragen, während die Kosten für das Eingeständnis von Fehlern anfallen.“ wird von einem selbst getragen. ”
Aufgrund der Natur von Threads können Sie keine Ausnahmen abfangen, die aus dem Thread entkommen. Sobald eine Ausnahme der run()
-Methode einer Aufgabe entgeht, wird sie an die Konsole weitergegeben, sofern Sie keine besonderen Schritte unternehmen, um solche fehlerhaften Ausnahmen abzufangen.
Single-Thread-Programme können als eine einzelne Einheit betrachtet werden, die die Problemdomäne löst und jeweils nur eine Sache tun kann.
Da das Flag canceled
vom Typ boolean
ist, ist es atomar, d. h. einfache Operationen wie Zuweisung und Rückgabewert sind vorhanden Dabei besteht keine Möglichkeit einer Unterbrechung, sodass sich die Domäne während der Ausführung dieser einfachen Vorgänge nicht in einem Zwischenzustand befindet.
Es ist wichtig zu beachten, dass der Inkrementierungsvorgang selbst mehrere Schritte erfordert und die Aufgabe während des Inkrementierungsprozesses durch den reinen Mechanismus angehalten werden kann – das heißt, in Java ist die Inkrementierung keine atomare Operation. Daher ist selbst ein einzelnes Inkrement nicht sicher, ohne die Aufgabe zu schützen.
unterbrechen und Executor
auf shutdownNow()
aufrufen, wird ein interrupt()
-Aufruf an alle Threads gesendet, die gestartet werden.
Executor
Indem Sie submit()
anstelle von excutor()
aufrufen, um eine Aufgabe zu starten, können Sie den Kontext der Aufgabe festhalten. submit()
gibt ein generisches Future<?>
zurück. Der Schlüssel zu diesem Future
liegt darin, dass Sie cancel()
darauf aufrufen und es daher verwenden können, um eine bestimmte Aufgabe zu unterbrechen. Wenn Sie true
an cancel()
übergeben, erhält es die Berechtigung, interrupt()
für diesen Thread aufzurufen, um ihn zu stoppen. Daher ist cancel()
eine Möglichkeit, einen einzelnen Thread zu unterbrechen, der von Excutor
gestartet wurde.
SleepBlock()
ist eine unterbrechbare Blockierung, während IOBlocked
und SynchronizedBlocked
eine unterbrechungsfreie Blockierung sind. Die Beispiele der drei oben genannten Klassen beweisen, dass E/A und Warten auf synchronized
-Blöcke unterbrechungsfrei sind. Weder E/A noch der Versuch, eine synchronized
-Methode aufzurufen, erfordern einen InterruptedException
-Handler.
Wie Sie der Ausgabe des Beispiels für die drei Klassen oben entnehmen können, können Sie den Aufruf von sleep()
(oder jeden Aufruf, der das Auslösen von InterruptedException
erfordert) unterbrechen. Sie können jedoch keinen Thread unterbrechen, der versucht, eine synchronized
-Sperre zu erhalten oder eine E/A-Operation auszuführen. Dies ist etwas ärgerlich, insbesondere bei der Ausführung von I/O-Aufgaben, da es bedeutet, dass IO das Potenzial hat, Ihr Multithread-Programm zu blockieren. Insbesondere bei webbasierten Programmen ist dies eine Frage des Risikos.
Eine etwas umständliche, aber manchmal effektive Lösung für diese Art von Problem besteht darin, die zugrunde liegende Ressource zu schließen, auf der die Aufgabe blockiert ist:
wait()
können Sie warten, bis sich eine bestimmte Bedingung ändert. Das Ändern dieser Bedingung liegt außerhalb der Kontrolle der aktuellen Methode. Oftmals wird dieser Zustand durch eine andere Aufgabe verändert. Sie möchten auf keinen Fall weiterhin eine leere Schleife ausführen, während Ihre Aufgabe diese Bedingung testet. Dies wird als „Busy Wait“ bezeichnet und ist im Allgemeinen eine schlechte Verwendung von Zyklen. Daher wird wait()
die Aufgabe anhalten, während auf Änderungen in der Außenwelt gewartet wird, und nur wenn notify()
oder notifyAll()
auftritt, was bedeutet, dass etwas Interessantes passiert ist, wird die Aufgabe aktiviert und überprüft die vorgenommenen Änderungen . Daher bietet wait()
eine Möglichkeit, Aktivitäten zwischen Aufgaben zu synchronisieren.
Die Sperre wird beim Aufruf von sleep()
nicht aufgehoben. Dies ist auch beim Aufruf von yield()
der Fall. Ein besonderer Aspekt von wait()
, notify()
und notifyAll()
ist, dass diese Methoden Teil der Basisklasse Object
und nicht Teil von Thread
sind.
Signal verpasst.
In der Diskussion über den Threading-Mechanismus von Java gibt es eine verwirrende Beschreibung: notifyAll()
weckt „alle Aufgaben, die in der Zukunft warten“ auf. . Bedeutet das, dass jede Aufgabe im Status wait()
irgendwo im Programm durch jeden Aufruf von notifyAll()
aktiviert wird? Es gibt Beispiele, bei denen dies nicht der Fall ist – tatsächlich werden, wenn notifyAll()
für eine bestimmte Sperre aufgerufen wird, nur Aufgaben aktiviert, die auf diese Sperre warten.
Das von Edsger Dijkstrar vorgeschlagene Problem der Speisephilosophen ist ein klassisches Beispiel für einen Deadlock.
Um das Deadlock-Problem zu beheben, müssen Sie verstehen, dass ein Deadlock auftritt, wenn die folgenden vier Bedingungen gleichzeitig erfüllt sind:
Sich gegenseitig ausschließende Bedingungen. Mindestens eine der von der Aufgabe verwendeten Ressourcen kann nicht gemeinsam genutzt werden. Hier kann ein Essstäbchen jeweils nur von einem Philosophen verwendet werden.
Mindestens eine Aufgabe muss eine Ressource enthalten und wartet darauf, eine Ressource abzurufen, die derzeit von einer anderen Aufgabe gehalten wird. Das heißt, damit es zu einem Deadlock kommt, muss der Philosoph ein Essstäbchen halten und auf ein anderes warten.
Ressourcen können nicht durch Aufgaben vorbelegt werden und Aufgaben müssen die Ressourcenfreigabe als normales Ereignis behandeln. Philosophen sind höflich und nehmen anderen Philosophen keine Stäbchen weg.
Es muss eine Warteschleife geben. Zu diesem Zeitpunkt wartet eine Aufgabe auf die Ressourcen, die von anderen Aufgaben gehalten werden, und diese wartet auf den Brei, der von einer anderen Aufgabe gehalten wird geht weiter, bis eine Aufgabe auf Ressourcen wartet, die von der ersten Aufgabe gehalten werden, wodurch alle gesperrt werden. Da in DeadlockingDiningPhilosophers.java jeder Philosoph zuerst versucht, das Stäbchen rechts und dann das Stäbchen links zu bekommen, kommt es zu einer Warteschleife.
Um einen Deadlock zu verhindern, müssen Sie nur einen von ihnen zerstören. Der einfachste Weg, einen Deadlock zu verhindern, besteht darin, Bedingung 4 zu verletzen.
Anwendbare Szenarien: Es wird verwendet, um eine oder mehrere Aufgaben zu synchronisieren und sie zu zwingen, auf die Ausführung durch andere zu warten Aufgaben Eine Reihe von Vorgängen ist abgeschlossen. Das heißt, eine oder mehrere Aufgaben müssen warten, bis andere Aufgaben, beispielsweise der erste Teil eines Problems, abgeschlossen sind.
Sie können einen Anfangswert für das CountDownLatch
-Objekt festlegen. Jede Methode, die wait() für dieses Objekt aufruft, wird blockiert, bis der Zählwert 0 erreicht. Wenn andere Faktoren ihre Arbeit beenden, können sie countDown() aufrufen. auf das Objekt, um die Anzahl zu verringern. CountDownLatch
Entwickelt, um nur einmal abgefeuert zu werden und der Zähler kann nicht zurückgesetzt werden. Wenn Sie eine Version benötigen, die den Zähler zurücksetzt, können Sie CyclicBarrier
verwenden.
Die Aufgabe, die countDown()
aufruft, wird bei diesem Aufruf nicht blockiert. Nur der Aufruf von await()
wird blockiert, bis der Zählwert 0
erreicht. Die typische Verwendung von
CountDownLatch
besteht darin, ein Programm in n
unabhängig lösbare Aufgaben zu unterteilen und n
mit Wert CountDownLatch
zu erstellen. Wenn jede Aufgabe abgeschlossen ist, wird countDown()
für diesen Latch aufgerufen. Aufgaben, die darauf warten, dass das Problem gelöst wird, rufen await()
für diesen Latch auf und halten sich selbst an, bis die Latch-Zählung abgelaufen ist.
Geeignet für Situationen, in denen Sie eine Reihe von Aufgaben erstellen möchten, die parallel arbeiten, und dann warten möchten, bis alle Aufgaben abgeschlossen sind, bevor Sie mit dem nächsten Schritt fortfahren (Sieht aus wie ein ein bisschen wie Join()). Es führt dazu, dass alle parallelen Aufgaben am Zaun in die Warteschlange gestellt werden und somit gleichmäßig voranschreiten.
Zum Beispiel ist das Programm Pferderennprogramm: HorseRace.java
DelayQueue
eine unbegrenzte BlockingQueue
(synchrone Warteschlange), die früher verwendet wurde Platzieren Sie die Implementierung Delayed
Das Objekt der Schnittstelle, dessen Objekt nur dann aus der Warteschlange genommen werden kann, wenn es abläuft. Diese Warteschlange ist geordnet, das heißt, das Hauptobjekt läuft als erstes ab. Wenn keine abgelaufenen Objekte vorhanden sind, hat die Warteschlange kein Kopfelement, sodass poll()
null
zurückgibt (aus diesem Grund können wir null
nicht in diese Art von Warteschlange einfügen). Wie oben erwähnt, wird DelayQueue
zu einer Variante der Prioritätswarteschlange.
Dies ist eine sehr einfache Prioritätswarteschlange mit blockierenden Lesevorgängen. Der blockierende Charakter dieser Warteschlange sorgt für die gesamte erforderliche Synchronisierung. Sie sollten daher beachten, dass hier keine explizite Synchronisierung erforderlich ist. Sie müssen sich keine Gedanken darüber machen, ob Elemente in dieser Warteschlange vorhanden sind, wenn Sie daraus lesen, da dies der Fall ist queue Wenn keine Elemente vorhanden sind, wird der Leser direkt blockiert.
„Gewächshaussteuerungssystem“ kann als Parallelitätsproblem betrachtet werden, jedes gewünschte Gewächshausereignis ist eine Aufgabe, die zu einem geplanten Zeitpunkt ausgeführt wird. ScheduledThreadPoolExecutor
kann dieses Problem lösen. Unter anderem wird Schedule() verwendet, um die Aufgabe einmal auszuführen, und ScheduleAtFixedRate() führt die Aufgabe zu jedem angegebenen Zeitpunkt wiederholt aus. Beide Methoden erhalten den Parameter „delayTime“. Ausführbare Objekte können so eingestellt werden, dass sie zu einem späteren Zeitpunkt ausgeführt werden.
BlockingQueue
: Synchrone Warteschlange: Wenn das erste Element leer oder nicht verfügbar ist, wird beim Ausführen von .take () gewartet (Blockierung, Blockierung).
SynchronousQueue
: ist eine blockierende Warteschlange ohne interne Kapazität, daher muss jeder put() auf einen take() warten und umgekehrt (d. h. jeder take() muss auf einen put() warten)) . Es ist, als würden Sie jemandem einen Gegenstand geben – es gibt keinen Tisch, auf den Sie den Gegenstand legen können, Sie können also nur arbeiten, wenn die Person die Hand ausstreckt und bereit ist, den Gegenstand entgegenzunehmen. In diesem Fall stellt die SynchronousQueue einen Standort vor dem Restaurant dar, um das Konzept zu untermauern, dass jeweils nur ein Gericht serviert werden kann.
Eine sehr wichtige Sache, die man bei diesem Beispiel beobachten sollte, ist die Verwaltungskomplexität der Verwendung von Warteschlangen für die Kommunikation zwischen Aufgaben. Diese einzelne Technik vereinfacht den Prozess der gleichzeitigen Programmierung erheblich, indem sie die Kontrolle umkehrt: Aufgaben stören sich nicht direkt gegenseitig, sondern senden Objekte über Warteschlangen aneinander. Die Empfangsaufgabe verarbeitet das Objekt und behandelt es als Nachricht, anstatt ihm Nachrichten zu senden. Wenn Sie diese Technik wann immer möglich anwenden, erhöhen sich Ihre Chancen, ein robustes gleichzeitiges System aufzubauen, erheblich.
Die Gefahren des „Microbenchmarking“: Dieser Begriff bezieht sich allgemein auf Leistungstests einer Funktion isoliert und außerhalb des Kontexts. Natürlich müssen Sie immer noch Tests schreiben, um Aussagen wie „Lock ist schneller als synchronisiert“ zu überprüfen, aber Sie müssen beim Schreiben dieser Tests wissen, was tatsächlich während der Kompilierung und zur Laufzeit passiert.
Verschiedene Compiler und Laufzeitsysteme variieren in dieser Hinsicht, daher ist es schwierig, genau zu wissen, was passieren wird, aber wir müssen verhindern, dass der Compiler die Möglichkeit des Ergebnisses vorhersagt.
Die Verwendung von Lock ist normalerweise viel effizienter als die Verwendung von synchronisiert, und der Overhead von synchronisiert scheint zu stark zu variieren, während Lock relativ konsistent ist.
Bedeutet das, dass Sie niemals das synchronisierte Schlüsselwort verwenden sollten? Hier sind zwei Faktoren zu berücksichtigen:
Erstens die Größe des Methodenkörpers der sich gegenseitig ausschließenden Methode.
Zweitens ist der durch das synchronisierte Schlüsselwort generierte Code besser lesbar als der Code, der durch die von Lock geforderte Redewendung „lock-try/finally-unlock“ generiert wird.
Code wird viel häufiger gelesen als geschrieben. Beim Programmieren ist die Kommunikation mit anderen Menschen viel wichtiger als die Kommunikation mit dem Computer, daher ist die Lesbarkeit Ihres Codes entscheidend. Daher ist es von praktischer Bedeutung, mit dem synchronisierten Schlüsselwort zu beginnen und es erst während der Leistungsoptimierung durch ein Lock-Objekt zu ersetzen.
Die allgemeine Strategie für diese sperrenfreien Fenster lautet: Änderungen am Container können gleichzeitig mit Lesevorgängen erfolgen, solange der Leser nur Sie sehen kann die Ergebnisse abgeschlossener Modifikationen. Änderungen werden an einer separaten Kopie eines Teils der Datenstruktur des Containers durchgeführt (manchmal einer Kopie der gesamten Datenstruktur), und diese Kopie ist während des Änderungsprozesses nicht sichtbar. Erst wenn die Änderung abgeschlossen ist, wird die geänderte Struktur automatisch mit der Hauptdatenstruktur ausgetauscht, und der Leser kann die Änderung sehen.
Optimistisches Sperren
Solange Sie hauptsächlich aus einem sperrenfreien Container lesen, ist dies viel schneller als sein synchronisiertes Gegenstück, da der Aufwand für das Erfassen und Freigeben von Sperren entfällt. Dies ist immer noch der Fall, wenn eine kleine Anzahl von Schreibvorgängen in einen sperrenfreien Container durchgeführt werden muss, aber was zählt als „kleine Menge“? Das ist eine sehr interessante Frage.
Ein zusätzlicher Vorteil von Threads besteht darin, dass sie leichte Ausführungskontextwechsel (ca. 100 Anweisungen) anstelle schwerer Prozesskontextwechsel (Tausende von Anweisungen) bereitstellen. Da sich alle Threads innerhalb eines bestimmten Prozesses denselben Speicherplatz teilen, ändert der einfache Kontextwechsel nur die Ausführungssequenz und die lokalen Variablen des Programms. Prozessschalter (schwergewichtige Kontextschalter) müssen den gesamten Speicherplatz ändern.
Verwandte Artikel:
Java-Programmiergedanken-Lernklasse (6) Kapitel 19 – Aufgezählte Typen
Java-Programmiergedanken-Lernklasse (7) Kapitel 20 – Anmerkungen
Das obige ist der detaillierte Inhalt vonJava-Programmiergedanken-Lernklasse (8) Kapitel 21 – Parallelität. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!