Die gleichzeitige Programmierung ist eine der wichtigsten Fähigkeiten für Java -Programmierer und eine der schwierigsten Fähigkeiten, die man beherrschen kann. Programmierer müssen ein tiefes Verständnis der niedrigsten Betriebsprinzipien des Computers haben, und gleichzeitig müssen Programmierer eindeutig logik und akribisch denken, damit sie effiziente, sichere und zuverlässige Multi-Thread-Programme schreiben können. Diese Serie beginnt von der Art der Koordination zwischen den Threads (Warten, Benachrichtigung, Benachrichtigung), synchronisiert und flüchtig und erläutern Sie jedes von JDK bereitgestellte Implementierungsmechanismus ausführlich. Auf dieser Basis werden wir die Werkzeugklassen des Java.util.Concurrent -Pakets, einschließlich der Nutzung, der Implementierung des Quellcode und der darin enthaltenen Prinzipien, weiter analysieren. Dieser Artikel ist der erste Artikel in dieser Serie und der kernste theoretische Teil dieser Serie. Nachfolgende Artikel werden basierend darauf analysiert und erklärt.
1. Teilen
Der Datenaustausch ist einer der Hauptgründe für die Sicherheit von Threads. Wenn alle Daten nur im Thread gültig sind, gibt es kein Problem mit der Sicherheit von Threads, was einer der Hauptgründe ist, warum wir beim Programmieren häufig keine Gewindesicherheit berücksichtigen müssen. Bei der Multithread -Programmierung ist die Datenaustausch jedoch unvermeidlich. Das typischste Szenario sind die Daten in der Datenbank. Um die Konsistenz der Daten zu gewährleisten, müssen wir die Daten normalerweise in derselben Datenbank weitergeben. Selbst bei Master und Sklaven werden auf die gleichen Daten zugegriffen. Der Master und der Slave kopieren gerade die gleichen Daten für die Effizienz von Zugriff und Datensicherheit. Wir demonstrieren nun die Probleme, die durch das Teilen von Daten unter mehreren Threads durch ein einfaches Beispiel geteilt werden:
Code -Snippet 1:
Paket com.paddx.test.concurrent; öffentliche Klasse Sharedata {public static int count = 0; public static void main (String [] args) {endgültige Sharedata data = new Sharedata (); für (int i = 0; i <10; i ++) {neuer Thread (new Runnable () {@Override public void run () {try {// Pause für 1 Millisekunde, wenn er die Chance von Concurrency -Problemen erhöht. Data.AddCount (); } try {// Das Hauptprogramm wird 3 Sekunden lang durchgeführt, um sicherzustellen, dass die obige Programmausführung abgeschlossen ist. Sleep (3000); } catch (interruptedException e) {e.printstacktrace (); } System.out.println ("count =" + count); } public void addCount () {count ++; }}Der Zweck des oben genannten Code besteht darin, einen Vorgang hinzuzufügen, um das 1.000 -fache auszuführen. Hier wird jedoch über 10 Threads implementiert. Jeder Thread wird 100 -mal ausgeführt, und unter normalen Umständen sollten 1.000 ausgegeben werden. Wenn Sie das obige Programm jedoch ausführen, werden Sie feststellen, dass das Ergebnis nicht der Fall ist. Hier ist das Ausführungsergebnis einer bestimmten Zeit (die Ergebnisse jedes Laufs sind möglicherweise nicht gleich, und manchmal kann das richtige Ergebnis erzielt werden):
Es ist ersichtlich, dass für gemeinsame variable Operationen verschiedene unerwartete Ergebnisse in einer Umgebung mit mehreren Threaden leicht zu sehen sind.
2. gegenseitiger Ausschluss
Der gegenseitige Ausschluss von Ressourcen bedeutet, dass nur ein Besucher gleichzeitig darauf zugreifen darf, was einzigartig und exklusiv ist. Normalerweise erlauben wir mehrere Threads, Daten gleichzeitig zu lesen, aber nur ein Thread kann gleichzeitig Daten schreiben. Daher teilen wir normalerweise Schlösser in gemeinsam genutzte Schlösser und exklusiven Schlösser, auch als Read -Sperren und Schreibschlösser bezeichnet. Wenn sich Ressourcen nicht gegenseitig ausschließen, müssen wir uns keine Sorgen um die Sicherheit von Threads machen, selbst wenn es sich um gemeinsame Ressourcen handelt. Zum Beispiel können alle Threads für unveränderliche Datenaustausch nur lesen, sodass keine Probleme mit der Sicherheit von Threads erforderlich sind. Das Schreiben von Operationen für gemeinsame Daten erfordern jedoch im Allgemeinen einen gegenseitigen Ausschluss. Im obigen Beispiel treten Probleme mit Datenmodifizierung aufgrund des Fehlens eines gegenseitigen Ausschlusses auf. Java bietet mehrere Mechanismen, um einen gegenseitigen Ausschluss zu gewährleisten. Der einfachste Weg ist die Verwendung synchronisiert. Jetzt fügen wir dem obigen Programm synchronisiert und führen wir aus:
Code -Snippet zwei:
Paket com.paddx.test.concurrent; öffentliche Klasse Sharedata {public static int count = 0; public static void main (String [] args) {endgültige Sharedata data = new Sharedata (); für (int i = 0; i <10; i ++) {neuer Thread (new Runnable () {@Override public void run () {try {// Pause für 1 Millisekunde, wenn er die Chance von Concurrency -Problemen erhöht. Data.AddCount (); } try {// Das Hauptprogramm wird 3 Sekunden lang durchgeführt, um sicherzustellen, dass die obige Programmausführung abgeschlossen ist. Sleep (3000); } catch (interruptedException e) {e.printstacktrace (); } System.out.println ("count =" + count); } / *** synchronisiertes Schlüsselwort hinzufügen* / public synchronisierte void addCount () {count ++; }}Nachdem der obige Code ausgeführt wird, werden Sie feststellen, dass das Endergebnis 1000 beträgt, egal wie oft Sie ausgeführt werden.
III. Atomizität
Atomizität bezieht sich auf den Betrieb von Daten als unabhängiges und unteilbares Ganzes. Mit anderen Worten, es ist eine Operation, die kontinuierlich und ununterbrochen ist. Die Hälfte der Datenausführung wird nicht durch andere Threads geändert. Der einfachste Weg, um sicherzustellen, dass Atomizität das Betriebssystemanweisungen ist. Wenn ein Betrieb einer Betriebssystemanweisung jeweils entspricht, wird dies definitiv die Atomizität gewährleisten. Viele Operationen können jedoch nicht mit einer Anweisung abgeschlossen werden. Zum Beispiel müssen für Langzeitbetriebe viele Systeme in mehrere Anweisungen unterteilt werden, um auf den hohen und niedrigen Positionen zu arbeiten. Zum Beispiel muss die Operation der Ganzzahl i ++, die wir häufig verwenden, tatsächlich in drei Schritte unterteilt werden: (1) Lesen Sie den Wert der Ganzzahl I; (2) eine Operation zu i hinzufügen; (3) Schreiben Sie das Ergebnis zurück in den Speicher. Dieser Prozess kann bei Multithreading auftreten:
Dies ist auch der Grund, warum das Ergebnis der Ausführung des Codesegments falsch ist. Für diesen Kombinationsbetrieb besteht die häufigste Möglichkeit, um die Atomizität zu gewährleisten, um zu sperren, z. B. synchronisierte oder sperren Java kann implementiert werden, und das Code -Segment 2 wird durch synchronisiert implementiert. Zusätzlich zu Schlössern gibt es eine andere Möglichkeit zu CAS (vergleichen und tauschen), dh vor dem Ändern der Daten vergleichen Sie, ob die vor den vorherigen gelesenen Werte konsistent sind. Wenn sie konsistent sind, modifizieren Sie sie und wenn sie inkonsistent sind, werden sie erneut ausgeführt. Dies ist auch das Prinzip der Optimierung der Schlossimplementierung. CAS ist jedoch in einigen Szenarien möglicherweise nicht wirksam. Ein weiterer Thread ändert beispielsweise zuerst einen bestimmten Wert und ändert ihn dann wieder in den ursprünglichen Wert. In diesem Fall kann CAS nicht beurteilen.
4. Sichtbarkeit
Um die Sichtbarkeit zu verstehen, müssen Sie ein gewisses Verständnis des Speichermodells des JVM haben. Das Speichermodell des JVM ähnelt dem Betriebssystem, wie in der Abbildung gezeigt:
Aus dieser Abbildung können wir sehen, dass jeder Thread seinen eigenen Arbeitsspeicher hat (entspricht dem CPU Advanced Puffer. Der Zweck besteht darin, den Geschwindigkeitsunterschied zwischen dem Speichersystem und der CPU weiter zu engagieren und die Leistung zu verbessern). Bei gemeinsam genutzten Variablen liest der Thread jedes Mal eine Kopie der gemeinsam genutzten Variablen im Arbeitsspeicher. Beim Schreiben ändert es direkt den Wert der Kopie im Arbeitsspeicher und synchronisiert dann den Arbeitsspeicher mit dem Wert im Hauptspeicher zu einem bestimmten Zeitpunkt. Das Problem, das dies verursacht, ist, dass, wenn Thread 1 eine bestimmte Variable modifiziert, Thread 2 möglicherweise nicht die von Thread 1 an der gemeinsam genutzten Variablen vorgenommenen Modifikationen angezeigt. Durch das folgende Programm können wir das unsichtbare Problem demonstrieren:
Paket com.paddx.test.concurrent; public class Visibilitytest {private static boolean Ready; private statische Int -Nummer; private statische Klasse Readerthread erweitert Thread {public void run () {try {thread.sleep (10); } catch (interruptedException e) {e.printstacktrace (); } if (! bereit) {System.out.println (bereit); } System.out.println (Nummer); }} private statische Klasse writer thread erweitert Thread {public void run () {try {thread.sleep (10); } catch (interruptedException e) {e.printstacktrace (); } number = 100; bereit = wahr; }} public static void main (String [] args) {new writer thread (). start (); new readerthread (). start (); }}Intuitiv sollte dieses Programm nur 100 ausgeben, und der Bereitschaftswert wird nicht gedruckt. Wenn Sie den obigen Code mehrmals ausführen, kann es viele verschiedene Ergebnisse geben. Hier sind die Ergebnisse von zwei Läufen:
Natürlich kann dieses Ergebnis nur aufgrund von Sichtbarkeit möglich sein. Wenn der Write -Thread (writer thead) bereit ist Für das zweite Ergebnis, dh das Ergebnis des Write -Threads, wurde bei der Ausführung if (! Ready) nicht gelesen, sondern das Ergebnis der Write -Thread -Ausführung wird beim Ausführen von System.out.println (bereit) gelesen. Dieses Ergebnis kann jedoch auch durch alternative Ausführung von Threads verursacht werden. Die Sichtbarkeit kann durch synchronisiertes oder flüchtiges in Java sichergestellt werden, und die spezifischen Details werden in nachfolgenden Artikeln analysiert.
5. Sequenz
Um die Leistung zu verbessern, kann der Compiler und der Prozessor Anweisungen neu ordnen. Es gibt drei Arten der Neuordnung:
(1) Compiler-optimiertes Neubestehen. Der Compiler kann die Ausführungsreihenfolge von Aussagen neu planen, ohne die Semantik eines einsthread-Programms zu ändern.
(2) Parallelität auf Unterrichtsebene. Moderne Prozessoren verwenden parallele Technologie (Anweisungsstufe), um die Ausführung mehrerer Anweisungen zu überlappen. Wenn keine Datenabhängigkeit vorliegt, kann der Prozessor die Ausführungsreihenfolge der Anweisung, die den Maschinenanweisungen entspricht, ändern.
(3) Neuordnung des Speichersystems. Da der Prozessor Cache- und Lese-/Schreibpuffer verwendet, scheinen das Laden und Speichervorgänge aus der Reihenfolge ausgeführt zu werden.
Wir können direkt auf die Beschreibung der Nachbestellung von Problemen in JSR 133 verweisen:
(1) (2)
Schauen wir uns zunächst den Quellcode -Teil (1) im obigen Bild an. Aus dem Quellcode wird entweder Anweisung 1 zuerst ausgeführt oder der Anweisungen 3 wird zuerst ausgeführt. Wenn Anweisung 1 zuerst ausgeführt wird, sollte R2 nicht den in Anweisung 4 geschriebenen Wert sehen. Wenn Anweisung 3 zuerst ausgeführt wird, sollte R1 nicht den Wert sehen, der durch Anweisung 2 geschrieben wurde. Das laufende Ergebnis kann jedoch R2 == 2 und R1 == 1 haben, was das Ergebnis der "Neuordnung" ist. Die obige Abbildung (2) ist ein mögliches Ergebnis der rechtlichen Zusammenstellung. Nach der Zusammenstellung kann die Reihenfolge der Anweisung 1 und Anweisung 2 austauscht werden. Daher wird das Ergebnis von R2 == 2 und R1 == 1 angezeigt. Synchronisierte oder flüchtige Möglichkeiten können auch in Java verwendet werden, um die Reihenfolge sicherzustellen.
Sechs Zusammenfassung
In diesem Artikel werden die theoretische Grundlage der gleichzeitigen Programmierung von Java erläutert, und einige Dinge werden in der nachfolgenden Analyse ausführlicher erörtert, z. B. Sichtbarkeit, Ordnung usw. Die nachfolgenden Artikel werden anhand des Inhalts dieses Kapitels erörtert. Wenn Sie den oben genannten Inhalt gut verstehen können, glaube ich, dass es Ihnen von großer Hilfe sein wird, ob Sie andere gleichzeitige Programmierartikel oder in Ihrer täglichen Programmierarbeit verstehen.