Dieser Artikel vermittelt Ihnen relevantes Wissen über Java, in dem hauptsächlich Probleme im Zusammenhang mit der gleichzeitigen Programmierung behandelt werden, einschließlich des Java-Speichermodells, einer detaillierten Erklärung von Volatile und des Implementierungsprinzips von Synchronized usw. Schauen wir uns das gemeinsam an es hilft allen.
Empfohlenes Studium: „Java-Video-Tutorial“
Das Java-Speichermodell ist das Java Memory Model, kurz JMM. JMM definiert, wie die Java Virtual Machine (JVM) im Computerspeicher (RAM) funktioniert. JVM ist das gesamte virtuelle Computermodell, daher ist JMM mit JVM verbunden. Die Java1.5-Version wurde überarbeitet und das aktuelle Java verwendet immer noch die Java1.5-Version. Die bei Jmm auftretenden Probleme ähneln denen moderner Computer.
Parallelitätsprobleme bei physischen Computern weisen viele Ähnlichkeiten mit den Situationen bei virtuellen Maschinen auf. Das Parallelitätsbehandlungsschema physischer Maschinen hat auch erhebliche Referenzbedeutung für die Implementierung virtueller Maschinen. D Laut dem „Report of Jeff Dean in the GOOGLE Plot of the Google“ können wir sehen, dass
Die folgenden Fälle dienen nur zur Veranschaulichung und stellen nicht die tatsächliche Situation dar.Wie lange dauert es, wenn 1 MB Daten vom Typ int aus dem Speicher gelesen und von der CPU akkumuliert werden?
Führen Sie eine einfache Berechnung durch. Der int-Typ in Java beträgt insgesamt 1024 * 1024/4 = 262144 Ganzzahlen Wir wissen, dass das Lesen von 1 Million Daten aus dem Speicher 250.000 Nanosekunden dauert. Zwischen beiden besteht zwar eine Lücke (diese Lücke ist natürlich nicht klein, 100.000 Nanosekunden reichen aus, damit die CPU fast 200.000 Anweisungen ausführen kann). immer noch in der gleichen Größenordnung. Ohne Caching-Mechanismus bedeutet dies jedoch, dass jede Zahl aus dem Speicher gelesen werden muss. In diesem Fall dauert es 100 Nanosekunden, bis die CPU den Speicher einmal liest, und 262144 Ganzzahlen werden aus dem Speicher in die CPU gelesen Die Berechnungszeit beträgt 262144
100+250000 = 26 464 400 Nanosekunden, was einen Unterschied in der Größenordnung darstellt. Und in Wirklichkeit können die meisten Computeraufgaben nicht einfach durch „Rechnen“ durch den Prozessor erledigt werden. Der Prozessor muss zumindest mit dem Speicher interagieren, wie z. B. das Lesen von Computerdaten, das Speichern von Computerergebnissen usw. Diese E/A-Operation ist es Im Grunde ist es unmöglich, sie zu beseitigen (es ist nicht möglich, sich bei der Erledigung aller Rechenaufgaben allein auf Register zu verlassen). Die Geschwindigkeiten der CPU und des Speichers waren in frühen Computern nahezu gleich, aber in modernen Computern übersteigt die Befehlsgeschwindigkeit der CPU die Zugriffsgeschwindigkeit des Speichers bei weitem, da zwischen dem Speichergerät des Computers eine Lücke von mehreren Größenordnungen besteht und der Rechengeschwindigkeit des Prozessors, moderne Computer Computersysteme müssen eine Cache-Schicht (Cache) mit einer Lese- und Schreibgeschwindigkeit hinzufügen, die möglichst nahe an der Betriebsgeschwindigkeit des Prozessors liegt, als Puffer zwischen dem Speicher und dem Prozessor: die benötigten Daten Der Vorgang wird in den Cache kopiert, sodass der Vorgang schnell ausgeführt werden kann. Wenn der Vorgang abgeschlossen ist, wird er aus dem Cache wieder mit dem Speicher synchronisiert, sodass der Prozessor nicht auf langsame Lese- und Schreibvorgänge im Speicher warten muss.
In einem Computersystem ist das Register der L0-Level-Cache, gefolgt von L1, L2 und L3 (gefolgt von Speicher, lokaler Festplatte und Remote-Speicher). Der Cache-Speicherplatz weiter oben ist kleiner, die Geschwindigkeit ist höher und die Kosten sind höher; der Speicherplatz weiter unten ist größer, die Geschwindigkeit ist langsamer und die Kosten sind niedriger. Von oben nach unten kann jede Schicht als Cache der nächsten Schicht betrachtet werden, das heißt: Das L0-Register ist der Cache des L1-Cache der ersten Ebene, L1 ist der Cache der L2 und so weiter Jede Schicht stammt von der darunter liegenden Schicht, sodass die Daten jeder Schicht eine Teilmenge der Daten der nächsten Schicht sind.
Bei modernen CPUs sind im Allgemeinen L0, L1, L2 und L3 in die CPU integriert, und L1 ist außerdem in einen Datencache der ersten Ebene (Datencache, D-Cache, L1d) und einen unterteilt Befehlscache der ersten Ebene (Befehlscache, I-Cache, L1i), der jeweils zum Speichern von Daten und zum Ausführen der Befehlsdecodierung von Daten verwendet wird. Jeder Kern verfügt über eine unabhängige Recheneinheit, einen Controller, ein Register sowie einen L1- und L2-Cache, und dann teilen sich mehrere Kerne einer CPU die letzte Schicht des CPU-Cache L3.
Aus abstrakter Sicht definiert JMM die abstrakte Beziehung zwischen Threads und dem Hauptspeicher: Gemeinsam genutzte Variablen zwischen Threads werden im Hauptspeicher (Hauptspeicher) gespeichert, und jeder Thread hat eine eigene Lokaler Speicher (Local Memory), der eine Kopie der gemeinsam genutzten Variablen speichert, die der Thread lesen/schreiben kann. Lokaler Speicher ist ein abstraktes Konzept von JMM und existiert nicht wirklich. Es umfasst Caches, Schreibpuffer, Register und andere Hardware- und Compiler-Optimierungen.
Sichtbarkeit bedeutet, dass, wenn mehrere Threads auf dieselbe Variable zugreifen und ein Thread den Wert der Variablen ändert, andere Threads den geänderten Wert sofort sehen können.
Da alle Operationen an Variablen durch Threads im Arbeitsspeicher ausgeführt werden müssen und Variablen im Hauptspeicher nicht direkt gelesen und geschrieben werden können, befinden sich die gemeinsam genutzten Variablen V zunächst in ihrem eigenen Arbeitsspeicher und werden dann mit dem Hauptspeicher synchronisiert. Allerdings wird es nicht rechtzeitig in den Hauptspeicher geleert, sondern es entsteht ein gewisser Zeitunterschied. Offensichtlich ist die Operation von Thread A an der Variablen V zu diesem Zeitpunkt für Thread B nicht mehr sichtbar.
Um das Problem der Sichtbarkeit gemeinsamer Objekte zu lösen, können wir das Schlüsselwort volatile oder lock verwenden.
Atomizität: Das heißt, eine Operation oder mehrere Operationen werden entweder alle ausgeführt und der Ausführungsprozess wird durch keine Faktoren unterbrochen, oder sie werden überhaupt nicht ausgeführt.
Wir alle wissen, dass CPU-Ressourcen in Thread-Einheiten zugewiesen und zeitgesteuert aufgerufen werden. Das Betriebssystem ermöglicht die Ausführung eines Prozesses für einen kurzen Zeitraum, z. B. 50 Millisekunden wählt einen Prozess erneut aus (wir nennen es „Aufgabenwechsel“), diese 50 Millisekunden werden als „Zeitscheibe“ bezeichnet. Die meisten Aufgaben werden nach Ablauf des Zeitabschnitts gewechselt.
Warum verursacht der Thread-Wechsel Fehler?
Da das Betriebssystem einen Taskwechsel durchführt, kann dieser nach der Ausführung eines CPU-Befehls erfolgen! Beachten Sie, dass es sich um eine CPU-Anweisung, CPU-Anweisung, CPU-Anweisung und nicht um eine Anweisung in einer Hochsprache handelt. Beispielsweise ist count++ in Java nur ein Satz, in höheren Sprachen erfordert eine Anweisung jedoch häufig mehrere CPU-Anweisungen, um ausgeführt zu werden. Tatsächlich enthält count++ mindestens drei CPU-Anweisungen! 那么线程切换为什么会带来 bug 呢?
因为操作系统做任务切换,可以发生在任何一条 CPU 指令执行完!注意,是 CPU 指令,CPU 指令,CPU 指令,而不是高级语言里的一条语句。比如 count++,在 java 里就是一句话,但高级语言里一条语句往往需要多条 CPU 指令完成。其实 count++至少包含了三个 CPU 指令!
可以把对 volatile 变量的单个读/写
,看成是使用同一个锁对这些单个读/写
Sie können einen einzelnen
Lesen/Schreiben
einer flüchtigen Variablen so betrachten, als würden Sie dieselbe Sperre zum Lesen/Schreiben dieser einzelnen Codes> verwenden ist synchronisiertpublic class Volati { // 使用volatile 声明一个64位的long型变量 volatile long i = 0L;// 单个volatile 变量的读 public long getI() { return i; }// 单个volatile 变量的写 public void setI(long i) { this.i = i; }// 复合(多个)volatile 变量的 读/写 public void iCount(){ i ++; }}
kann als folgender Code gesehen werden:
public class VolaLikeSyn { // 使用 long 型变量 long i = 0L; public synchronized long getI() { return i; }// 对单个的普通变量的读用同一个锁同步 public synchronized void setI(long i) { this.i = i; }// 普通方法调用 public void iCount(){ long temp = getI(); // 调用已同步的读方法 temp = temp + 1L; // 普通写操作 setI(temp); // 调用已同步的写方法 }}
: Das Lesen/Schreiben einer einzelnen flüchtigen Variablen ist atomar, aber zusammengesetzte Operationen wie volatile++ sind nicht atomar.
Obwohl volatile sicherstellen kann, dass Variablen rechtzeitig nach der Ausführung in den Hauptspeicher geleert werden, hat Thread A für count++, eine nicht-atomare Situation mit mehreren Anweisungen, aufgrund des Threadwechsels gerade count=0 in den Arbeitsspeicher geladen , und Thread B hat gerade count=0 in den Arbeitsspeicher geladen. Dadurch sind die Ausführungsergebnisse von Thread A und B beide 1 und werden in den Hauptspeicher geschrieben Speicher ist immer noch 1, nicht 2 Die Implementierung von Synchronized in JVM basiert auf dem Eingeben und Verlassen von Monitorobjekten, um Methodensynchronisation und Codeblocksynchronisation zu erreichen. Obwohl die spezifischen Implementierungsdetails unterschiedlich sind, können sie alle über gepaarte MonitorEnter implementiert werden und MonitorExit-Direktive zu erreichen.
Bei synchronisierten Blöcken wird die MonitorEnter-Anweisung am Anfang des synchronisierten Codeblocks eingefügt, während die monitorExit-Anweisung am Ende der Methode und Ausnahme eingefügt wird. Die JVM garantiert, dass jeder MonitorEnter über einen entsprechenden MonitorExit verfügen muss. Wenn der Code diese Anweisung ausführt, versucht er im Allgemeinen, den Besitz des Objektmonitors zu erlangen, das heißt, er versucht, die Sperre des Objekts zu erhalten:
Die von synchronisiert verwendete Sperre wird im Java-Objekt-Header gespeichert. Der Objekt-Header des Java-Objekts besteht aus zwei Teilen: Mark Word und Klass-Zeiger:
Die Sperrinformationen sind im Markierungswort des Objekts vorhanden. Die Standarddaten in MarkWord dienen zum Speichern des HashCodes und anderer Informationen des Objekts.
Aber es ändert sich, wenn sich der Betrieb des Objekts ändert.
, die mit der Konkurrenz allmählich eskalieren. Sperren können hochgestuft, aber nicht herabgestuft werden, um die Effizienz beim Erwerb und Freigeben von Sperren zu verbessern. 4.2. Voreingenommene Sperre
Einführender Hintergrund: In den meisten Fällen gibt es bei Sperren nicht nur keine Multi-Thread-Konkurrenz, sondern sie werden immer mehrfach von demselben Thread erworben, um den Erwerb von Sperren für Threads zu verbilligen , voreingenommene Sperren werden eingeführt. Reduzieren Sie unnötige CAS-Operationen. „Die voreingenommene Sperre wirkt sich, wie der Name schon sagt, auf den ersten Besuch des Threads aus. Wenn ein Thread während des Vorgangs nur auf die synchrone Sperre zugreift, liegt kein Multithread-Streit vor, und der Thread muss nicht ausgelöst werden synchronisieren, reduzieren, reduzieren Einige CAS-Vorgänge zum Sperren/Entsperren (z. B. einige CAS-Vorgänge in Warteschlangen). In diesem Fall wird dem Thread eine Bias-Sperre hinzugefügt. Wenn andere Threads die Sperre während des Betriebs verhindern, wird der Thread, der die voreingenommene Sperre hält, angehalten, und die JVM entfernt die voreingenommene Sperre für ihn und stellt die Sperre auf eine Standard-Lightweight-Sperre wieder her. Es verbessert die Laufleistung des Programms weiter, indem es Synchronisierungsprimitive eliminiert, wenn keine Konkurrenz um Ressourcen besteht.
Sehen Sie sich das Bild unten an, um den Prozess der Bias-Lock-Erfassung zu verstehen:
Schritt 1. Besuchen Sie Mark Word, um zu sehen, ob das Bias-Lock-Flag auf 1 gesetzt ist und ob das Lock-Flag 01 ist. Bestätigen Sie dies es befindet sich in einem voreingenommenen Zustand. Schritt 2. Wenn es sich im vorgespannten Zustand befindet, testen Sie, ob die Thread-ID auf den aktuellen Thread verweist. Wenn ja, fahren Sie mit Schritt 5 fort, andernfalls fahren Sie mit Schritt 3 fort.
Schritt 3. Wenn die Thread-ID nicht auf den aktuellen Thread verweist, konkurrieren Sie um die Sperre durch den CAS-Vorgang. Wenn der Wettbewerb erfolgreich ist, setzen Sie die Thread-ID in Mark Word auf die aktuelle Thread-ID und führen Sie dann 5 aus. Wenn der Wettbewerb fehlschlägt, führen Sie 4 aus. Schritt 4. Wenn es CAS nicht gelingt, die Bias-Sperre zu erlangen, bedeutet das, dass es Konkurrenz gibt. Beim Erreichen des globalen Sicherheitspunkts (Sicherheitspunkt) wird der Thread, der die Bias-Sperre erhält, angehalten, die Bias-Sperre wird auf eine leichte Sperre aktualisiert und dann führt der am Sicherheitspunkt blockierte Thread den Synchronisationscode weiterhin aus. (Das Aufheben der Bias-Sperre führt dazu, dass das Wort gestoppt wird.)
Schritt 5. Führen Sie den Synchronisierungscode aus.
Bias Lock Release:
Die Aufhebung der Voreingenommenheitssperre wird oben im vierten Schritt erwähnt. Nur wenn andere Threads versuchen, um die Bias-Sperre zu konkurrieren, gibt der Thread, der die Bias-Sperre hält, die Bias-Sperre frei, und der Thread gibt die Bias-Sperre nicht aktiv frei. Um die voreingenommene Sperre aufzuheben, müssen Sie auf den globalen Sicherheitspunkt warten (zu diesem Zeitpunkt wird kein Bytecode ausgeführt). Zuerst wird der Thread angehalten, der die voreingenommene Sperre besitzt, und feststellen, ob sich das Sperrobjekt in einem gesperrten Zustand befindet , und stellen Sie dann die voreingenommene Sperre auf den vorherigen Zustand zurück, nachdem Sie die voreingenommene Sperre aufgehoben haben. Der Status der Sperre (Flag-Bit ist „01“) oder der Lightweight-Sperre (Flag-Bit ist „00“).
Anwendbare Szenarien für voreingenommene Sperren:
Es gibt immer nur einen Thread, der den Synchronisationsblock ausführt, bevor er die Ausführung beendet und die Sperre aufhebt. Es wird verwendet, wenn kein anderer Thread vorhanden ist Sobald die Konkurrenz auf eine leichte Sperre aktualisiert wird, muss die voreingenommene Sperre aufgehoben werden Bei Sperren führt die voreingenommene Sperre viele zusätzliche Vorgänge aus. Insbesondere beim Aufheben der voreingenommenen Sperre führt dies zum Eintritt in den sicheren Punkt und zu einer Leistungseinbuße .
jvm Vorspannungssperre ein-/ausschalten4.3, leichte Sperre Von der Bias-Sperre aktualisiert, wird die Bias-Sperre ausgeführt, wenn ein Thread dem Sperrenkonflikt beitritt.Vorspannungssperre einschalten: -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 Vorspannungssperre ausschalten: -XX:-UseBiasedLocking
Lightweight-Sperre Der Sperrprozess:
Aber das Thread-Spinning erfordert CPU-Verbrauch, um es ganz klar auszudrücken: Der Thread kann die CPU nicht immer für nutzlose Arbeit beanspruchen, daher müssen Sie eine maximale Spin-Wartezeit festlegen.
Wenn die Ausführungszeit des Threads, der die Sperre hält, die maximale Spin-Wartezeit überschreitet und die Sperre nicht aufgehoben wird, können andere Threads, die um die Sperre konkurrieren, die Sperre immer noch nicht innerhalb der maximalen Wartezeit erhalten Der konkurrierende Thread stoppt sich selbst.
Wenn jedoch die Konkurrenz um die Sperre groß ist oder der Thread, der die Sperre hält, die Sperre lange Zeit belegen muss, um den Synchronisationsblock auszuführen, ist die Verwendung der Spin-Sperre zu diesem Zeitpunkt nicht geeignet, da die Spin-Sperre immer aktiviert ist Beansprucht die CPU für nutzlose Arbeit, bevor die Sperre erlangt wird. Der Verbrauch durch Thread-Spinning ist größer als der Verbrauch durch Thread-Blockierungs- und Suspendierungsvorgänge der CPU.
Der Zweck der Spin-Sperre besteht darin, die CPU-Ressourcen zu belegen, ohne sie freizugeben, und zu warten, bis die Sperre erworben wird, um sie sofort zu verarbeiten. Aber wie wählt man die Ausführungszeit des Spins? Wenn die Spin-Ausführungszeit zu lang ist, befindet sich eine große Anzahl von Threads im Spin-Zustand und belegt CPU-Ressourcen, was sich auf die Leistung des Gesamtsystems auswirkt. Daher ist die Anzahl der Spins wichtig. JVM wählt die Anzahl der Drehungen aus, JDK1.5 ist standardmäßig auf 10 eingestellt, und in 1.6 wurden adaptive Spin-Sperren eingeführt. Die adaptive Spin-Sperre bedeutet, dass die Spin-Zeit nicht festgelegt ist, sondern von der vorherigen Zeit zur vorherigen Zeit in der vorherigen Zeit Die vorherige Zeit wird durch die Spinzeit derselben Sperre und den Status des Sperrenbesitzers bestimmt. Grundsätzlich wird davon ausgegangen, dass die Zeit des Kontextwechsels eines Threads die beste ist.
In JDK1.6, -XX:+UseSpinning aktiviert die Spin-Sperre; nach JDK1.7 wird dieser Parameter entfernt und von jvm gesteuert;4.3.4, Vergleich verschiedener Sperren
Empfohlene Studie: „
Java-Video-Tutorial“
Das obige ist der detaillierte Inhalt vonWissenspunkte zur gleichzeitigen Programmierung für das Java-Thread-Lernen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!