Grundlagen der Java-Parallelverarbeitung

Einführung

Mit der Einführung der Streaming-API in den Java8-Standard ist es zu einer Umgebung geworden, in der die Parallelverarbeitungsprogrammierung, die tendenziell kompliziert ist, immer einfacher wird. Ich spüre diesen Vorteil auch sehr. Während solche Parallelverarbeitungscodierungsmethoden immer leistungsfähiger werden, möchte ich diesmal leider einen Überblick über die Grundlagen und nicht über die neueste Technologie geben. Wenn Sie es wissen, lesen Sie es bitte. Wenn Sie eine Android-App erstellen, aber nicht mit Parallelverarbeitung vertraut sind, habe ich sie in der Hoffnung geschrieben, dass Sie sie sorgfältig lesen werden.

Gewindesicher

Ist es plötzlich in Ordnung, dass mehrere Threads auf die folgenden Klassen zugreifen?

public class SomeSequence {

    private int value;

    public int getNextValue() {
        return value++;
    }
}

Die Antwort ist nein. In einer Umgebung mit einem Thread gibt es kein besonderes Problem. Im Fall einer Multithread-Umgebung können wir jedoch nicht garantieren, dass sie ordnungsgemäß funktioniert. Angenommen, das Inkrementieren von "Wert ++" ist keine einzelne Operation, sondern "Wert = 1"

  1. Lesen Sie den aktuellen Wert und er wird zurückgegeben (tmp = Wert).
  2. 1 wird zum Rückgabewert addiert (tmp + 1)
  3. Schreiben Sie einen neuen Wert (Wert = tmp) Ich mache so etwas. Wenn es beispielsweise einen A-Thread und einen B-Thread gibt, ruft ein Thread "getNextValue" auf, und ein B-Thread ruft auch "getNextValue" zu einem unglücklichen Zeitpunkt auf (zum Beispiel verarbeitet ein Thread 1-2), wird "value" gesetzt. Der gleiche inkrementierte Wert wird zurückgegeben. Das erwartete Ergebnis von Thread B besteht darin, den Wert bei Thread A zu erhöhen, sodass es keine Methode mehr ist, die das erwartete Ergebnis zurückgibt. Dieses Phänomen wird als ** Rennbedingung ** bezeichnet. In diesem Fall wird in den Spezifikationen nicht davon ausgegangen, dass die Werte dupliziert werden und die Situation eines Mangels noch lange anhält und es sich um einen traurigen Zustand handelt.

Die einzige Möglichkeit, dieses Problem zu beheben, ist die Synchronisierung.

public class SomeSequence {

    private int value;

    public synchronized int getNextValue() {
        return value++;
    }
}

Wenn Sie die "synchronisierte" Methode für "getNextValue ()" verwenden, tritt das obige Problem auf. Einfach ausgedrückt, mit "synchronisiert" kann diese Methode jeweils nur einen Thread ausführen. Selbst wenn die Threads A und B auf "getNextValue" zugreifen, kann Thread B nicht auf die Methoden der Klasse zugreifen, bis Thread A die Verarbeitung beendet. Infolgedessen kann der Wert in einem normalen Zustand aktualisiert werden. Diese Klasse stellt sicher, dass sie sich auch dann korrekt verhält, wenn mehrere Threads darauf zugreifen. Dies wird als ** thread-sicher ** bezeichnet. Dann sollten Sie allen "synchronisiert" hinzufügen. Es gibt jedoch einige Fallstricke. Ich werde das später erklären.

Atomic

Es gibt eine Operation wie "value ++" im obigen Beispielcode, aber ich habe festgestellt, dass wenn sie nicht threadsicher ist, es nicht garantiert funktioniert, wenn sie von mehreren Threads aufgerufen wird. Diese Operation muss ** atomar ** sein, um diesen Prozess threadsicher zu machen, ohne Konfliktbedingungen zu verursachen. Atomic wird als unteilbare Operation bezeichnet und bedeutet, dass es ausgeführt werden kann, nachdem die ausgeführte Änderungsoperation sicher abgeschlossen wurde, ohne andere Threads zu unterbrechen. Operationen wie "Wert ++" werden als "Lese-Änderungs-Schreib" -Operationen (Lesen-Ändern-Schreiben) bezeichnet, verursachen jedoch wahrscheinlich Konflikte und müssen atomar ausgeführt werden.

Darüber hinaus wird die folgende verzögerte Initialisierung auch als ** check-then-act bezeichnet und verursacht wahrscheinlich einen Konfliktzustand. Darüber hinaus wird der Status, in dem mehrere Elemente wie Blei, Ändern, Schreiben, Überprüfen, Zen und Handeln kombiniert werden, als ** zusammengesetzte Aktion ** bezeichnet.

public class AppComponent {
    
    private AppComponent instance = null;
    
    public AppComponent getInstance() {
        if (instance == null) {
            instance = new Instance();
        }
        return instance;
    }
}

Wie bereits erwähnt, werden Thread A und Thread B, wenn sie "getInstance" ausführen, zu unglücklichen Zeiten ausgeführt, bevor sie in das Instanzfeld gesetzt werden, und werden bei zwei "getInstance" -Aufrufen separat ausgeführt. Sie erhalten das Objekt. Dies wird auch als ** veraltete Beobachtung ** bezeichnet (alter Wert kurz vor dem Update). Um atomar zu arbeiten, betrachten Sie ** Lock **, den grundlegenden Mechanismus von Java. Sie können die Thread-Sicherheit auch erreichen, indem Sie die vorhandene * atomare Variablenklasse * wie unten gezeigt verwenden. Alle Aktionen, die auf getNextValue zugreifen, sind definitiv atomar.

public class SomeSequence {

    private AtomicInteger value = new AtomicInteger(0);

    public int getNextValue() {
        return value.getAndIncrement();
    }
}

Sperren

Java hat einen Mechanismus, um Atomizität als Funktion der Sprache selbst zu erzwingen. Dies ist das "synchronisierte", das ich zuvor geschrieben habe. synchronized hat zwei Rollen. ** Sperre ** und ** Blockierung **. In der folgenden Syntax ist das übergebene "dies" synchronisiert ein Verweis auf ein Objekt, das als Sperre (Schlüssel) fungiert, und einen durch diese Sperre geschützten Codeblock. Im obigen Beispielcode wird es als synchronisierte Methode (synchronisierte Methode) mit "synchronisiert" vor dem Methodennamen bezeichnet, und das Folgende wird als synchronisierter Block (synchronisierter Block) bezeichnet. Das Beispiel sperrt den gesamten Code, sodass er dieselbe Bedeutung hat wie die oben beschriebene synchronisierte Methode.

public class SomeSequence {

    private int value;

    public int getNextValue() {
        synchronized(this) {
            return value++;
        }
    }
}

Alle Java-Objekte (Singleton-Instanzen und alle Klassen) fungieren als Sperren für die Synchronisation und werden von der Sprache unterstützt. Dies wird als ** eindeutige Sperre ** (Monitorsperre) bezeichnet. Nur der Thread, der die Sperre erhalten kann, kann den Synchronisationsblock (Methode) ausführen und die Sperre aufheben, wenn der Synchronisationsblock beendet ist (auch wenn eine Ausnahme ausgelöst wird). Wenn Thread A die Sperre erhalten hat, wartet Thread B darauf, dass Thread A die Sperre aufhebt. Dies wird als ** Mutex ** (sich gegenseitig ausschließende Sperre) bezeichnet. Wenn Thread A die Sperre nicht aufhebt, wartet Thread B für immer (Dead Lock).

Wird der folgende Code übrigens blockiert?

public class Sequence {
    public synchronized int getNextValue() {
        return value++;
    }
}

public class LoggerSequence extends Sequence {
    public synchronized int getNextValue() {
         log.d("[in] ", "-- calling getNextValue()");
         return super.getNextValue();
    }
}

In Thread A erhält LoggerSequence die Sperre der Sequence-Klasse der Erweiterungsquelle, wenn die Synchronisationsmethode "getNextValue" aufgerufen wird. Darüber hinaus versucht die Superklassenmethode im Blockcode auch, die Sequenzklassensperre zu erhalten. Da ich die Sequenzsperre mit Logger Sequence erworben habe, warte ich wahrscheinlich auf eine Sperre, die niemals erworben werden kann. Ein Deadlock tritt jedoch nicht tatsächlich auf. Die eindeutigen Sperren von Java haben die Eigenschaft ** reentrant ** (reentrant), und die Anforderung ist erfolgreich, wenn der Thread, der die Sperre hält, versucht, dieselbe Sperre zu erhalten. Die JVM verfolgt den Eigentümer-Thread, erhöht die Anzahl jedes Mal, wenn sie eine Sperre erwirbt, und verringert sie, sobald die Sperre aufgehoben wird. Wenn derselbe Thread erneut versucht, dieselbe Sperre zu erhalten, wird die Anzahl der Sperren erhöht oder verringert.

Wenn Sie eine Statusvariable sperren, auf die mehrere Threads zugreifen, müssen die Sperrobjekte identisch sein. Wenn Sie beispielsweise solchen Code haben, ist er überhaupt nicht threadsicher. Wenn Thread A auf "getNextValue" zugreift und die Verarbeitung durchführt, wenn jemand auf "setObject" zugreift und sich das Sperrobjekt ändert, ändert sich der Sperrstatus und ein anderer Thread kann auf den Codeblock zugreifen. Es wird sein.

public class SomeSequence {

    private Object lock = new Object;
    private int value;

    pubic void setObject(Object obj) {
       lock = obj;
    }

    public int getNextValue() {
        synchronized(lock) {
            return value++;
        }
    }
}

Wenn Sie zuerst die Thread-Sicherheit sicher realisieren möchten, warum nicht "synchronisiert" zu allen hinzufügen? Ich glaube ich habe geschrieben. Zum Beispiel, wenn Sie den folgenden Code haben: Die Methode (Block), die zuvor "synchronisiert" wurde, kann jeweils nur einen Thread ausführen. Wenn die Threads A, B und C auf die "addNumber" dieser Klasse zugreifen, warten die Threads B und C möglicherweise lange auf die Verarbeitung von Thread A. Mit anderen Worten, obwohl es einfach ist, ist die Ausführungsleistung sehr schlecht.

public class SomeService {

    private BigInteger number;
    
    public synchronized BigInteger getNumber() {
        return number;
    }

    public synchronized void addNumber() {
        number.add(BigInteger.TEN);
        someHeavyRequest();
    }
}

Der folgende Code wurde geändert, um "synchronisiert" nur zur Nummer der gemeinsam genutzten Variablen hinzuzufügen und innerhalb dieses Blocks zu aktualisieren. Angenommen, der Code außerhalb des Blocks greift nur auf lokale Variablen zu, werden diese nicht von mehreren Threads gemeinsam genutzt und müssen nicht synchronisiert werden. Thread A gibt die Sperre frei, bevor someHeavyRequest ausgeführt wird, sodass die Parallelität nicht beeinträchtigt wird und die Thread-Sicherheit aufrechterhalten werden kann.

public class SomeService {
    private BigInteger number;
    
    public synchronized BigInteger getNumber() {
        return number;
    }

    public void addNumber() {
        synchronized(this) {
            number.add(BigInteger.TEN);
        }
        someHeavyRequest();
    }
}

Parallelität wie das Feinmachen des "synchronisierten" Blocks verbessert die Ausführungsleistung (beachten Sie, dass der Code auch entsprechend kompliziert wird). Wenn Sie eine Sperre verwenden, sollten Sie sich darüber im Klaren sein, dass der Code im "synchronisierten" Block dies tut. Das Erfassen und Freigeben einer Sperre ist jedoch mit einem Overhead verbunden. Wenn Sie sie also zu stark auflösen, kann dies die Ausführungsleistung beeinträchtigen. Selbst wenn Sie die Ausführungsleistung verbessern möchten, kann die Einbußen bei der Einfachheit (Blockierung der gesamten Methode) die Sicherheit beeinträchtigen. Wenn Sie also die Größe des synchronisierten Blocks festlegen, ist dies ein Entwurfsziel. Wir brauchen auch einen Kompromiss. Es ist jedoch NG, während der Verarbeitung eine Sperre zu haben, die lange dauert (umfangreiche Berechnungsverarbeitung, Kommunikation usw.).

schließlich

Grundsätzlich weiß ich nicht warum, aber es kommt einmal alle 1000 Mal vor, wie zum Beispiel ein Fehler, der auftritt? Die meisten Fehler, über die ich gesprochen habe, sind Parallelverarbeitungssysteme. In einem solchen Fall kann es eine Abkürzung zur Lösung des Problems sein, wenn Sie die grundlegende Parallelverarbeitung verstehen oder nicht. Multithread-bezogene Elemente wie die Streaming-API von Java werden immer leistungsfähiger. Wenn Sie diese Grundlagen jedoch verstehen, können Sie sie möglicherweise schneller übernehmen. Und bitte sag es dir. Nun, ich habe eine Rezension über Javas Parallelverarbeitung geschrieben. In Bezug auf die Parallelverarbeitung schreibe ich die Grundlagen der Grundlagen. Es gibt viele mehr. Diejenigen, die mehr wissen wollen, sind alt, aber ich empfehle, die folgenden Bücher zu lesen.

[JAVA Parallel Processing Programming](https://www.amazon.co.jp/Java%E4%B8%A6%E8%A1%8C%E5%87%A6%E7%90%86%E3%83%97 % E3% 83% AD% E3% 82% B0% E3% 83% A9% E3% 83% 9F% E3% 83% B3% E3% 82% B0-% E2% 80% 95% E3% 81% 9D% E3% 81% AE% E3% 80% 8C% E5% 9F% BA% E7% 9B% A4% E3% 80% 8D% E3% 81% A8% E3% 80% 8C% E6% 9C% 80% E6% 96% B0API% E3% 80% 8D% E3% 82% 92% E7% A9% B6% E3% 82% 81% E3% 82% 8B% E2% 80% 95-Brian-Goetz / dp / 4797337206)

Recommended Posts

Grundlagen der Java-Parallelverarbeitung
Java-Grundlagen
Java-Grundlagen
Grundlagen der Java-Programmierung
Java JAR-Grundlagen
Objektorientierte (Java) Grundlagen
Java Network Basics (Kommunikation)
Muscle Java Basics Tag 1
Grundlagen der Zeichenoperation (Java)
Java
Zusammenfassung der Grundlagen der Java-Sprache
Grundlagen der Java-Programmierung Practice-Switch-Anweisung
Erste Schritte mit Java Basics
Grundlagen der Java-Entwicklung ~ Übung (Array) ~
Bei der Java-Parallelverarbeitung bin ich mir nicht sicher
Java
[Java11] Stream Usage Summary -Basics-
[Java-Grundlagen] Was ist Klasse?
Java-Leistung Kapitel 5 Grundlagen der Garbage Collection
Java lernen (0)
Java studieren ―― 3
Java geschützt
[Java] Anmerkung
Rails Grundlagen
[Java] Modul
Java-Array
Java studieren ―― 9
Java Scratch Scratch
Java-Tipps, Tipps
Java-Methoden
Java-Methode
Java (Konstruktor)
Java-Array
[Java] ArrayDeque
Ruby-Grundlagen
Java (überschreiben)
Java Day 2018
Java-Zeichenfolge
Java (Array)
Java statisch
Java-Serialisierung
Java Anfänger 4
JAVA hat bezahlt
Memorandum des neuen Absolventen SES [Java-Grundlagen]
Java studieren ―― 4
Java (gesetzt)
Fragmentgrundlagen
Java-Shell-Sortierung
[Java] compareTo
Java studieren -5
JPA-Grundlagen 1
Java (Schnittstelle)
Parallelitätsmethode in Java mit grundlegendem Beispiel
Java-Memorandum
Java-Array
Java studieren ―― 1
[Java] Array
Docker-Grundlagen
ViewPager-Grundlagen