Heim  >  Artikel  >  Java  >  Java-Programmiergedanken-Lernklasse (8) Kapitel 21 – Parallelität

Java-Programmiergedanken-Lernklasse (8) Kapitel 21 – Parallelität

php是最好的语言
php是最好的语言Original
2018-08-09 15:01:541318Durchsuche

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.

21.2.1 Aufgaben definieren

  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.

21.2.3 Mit Executor

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.

21.2.4 Erzeugen von Rückgabewerten aus 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.

21.2.9 Codierungsvariationen

  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.

21.2.13 Thread-Gruppe

  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. ”

21.2.14 Ausnahmen abfangen

 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.

21.3 Gemeinsame Nutzung eingeschränkter Ressourcen

  Single-Thread-Programme können als eine einzelne Einheit betrachtet werden, die die Problemdomäne löst und jeweils nur eine Sache tun kann.

21.3.1 Falscher Zugriff auf Ressourcen

  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.

21.4 Beenden von Aufgaben

21.4.3 Wenn Sie

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:

21.5 Zusammenarbeit zwischen Threads

21.5.1 Mit wait() und notifyAll()

 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.

21.5.2 notify() und notifyAll()

  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.

21.6 Deadlock

  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.

21.7 Komponenten in der neuen Klassenbibliothek

21.7.1 CountDownLatch

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.

21.7.2 CyclicBarrier

  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

21.7.3 DelayQueue

  DelayQueue eine unbegrenzte BlockingQueue (synchrone Warteschlange), die früher verwendet wurde Platzieren Sie die Implementierung DelayedDas 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.

21.7.4 PriorityBlockingQueue

  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.

21.7.5 Raumtemperaturregler mit ScheduledExecutor

  „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.

21.7.6 Semaphre

21.8 Simulation

21.8.1 Bankangestellter

21.8.2 Restaurantsimulation

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.

21.8.3 Vertriebsarbeit

21.9 Leistungsoptimierung

21.9.1 Vergleich von Mutex-Technologien

 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.

21.9.2 Sperrenfreie Container

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.

21.11 Zusammenfassung

 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!

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