Lassen Sie uns zunächst einige optimistische Schlösser und pessimistische Schlösser einführen:
Pessimistisches Schloss: Nehmen Sie immer den schlimmsten Fall an. Jedes Mal, wenn ich die Daten bekomme, denke ich, dass andere sie ändern werden, damit ich sie jedes Mal sperren werde, wenn ich die Daten bekomme, damit andere sie blockieren, bis es das Schloss erhält. Herkömmliche relationale Datenbanken verwenden viele solcher Verriegelungsmechanismen, wie z. B. Zeilensperrungen, Tabellenverriegelungen usw., Lesen von Sperren, Schreiben von Schlössern usw., die vor Vorgängen gesperrt sind. Beispielsweise ist die Implementierung des synchronisierten Schlüsselworts Synchronisierter Schlüsselwort in Java ebenfalls eine pessimistische Sperre.
Optimistisches Schloss: Wie der Name schon sagt, bedeutet dies sehr optimistisch. Jedes Mal, wenn ich die Daten bekomme, denke ich, dass andere sie nicht ändern werden, sodass ich sie nicht sperren werde. Bei der Aktualisierung werde ich jedoch beurteilen, ob andere die Daten in diesem Zeitraum aktualisiert haben und die Versionsnummer und andere Mechanismen verwenden können. Optimistische Schlösser eignen sich für Multi-Lead-Anwendungstypen, die den Durchsatz verbessern können. Beispielsweise bietet die Datenbank eine optimistische Sperre, die dem Mechanismus write_condition ähnelt, aber sie werden tatsächlich alle von optimistischen Sperren bereitgestellt. In Java wird die Atomvariablenklasse unter der Paket von Java.util.Concurrent.atomic von CAS unter Verwendung eines optimistischen Schlosses implementiert.
Eine Implementierung von optimistischen Lock-Cas (vergleichen und tauschen):
Sperrprobleme:
Vor JDK1.5 stützte sich Java auf synchronisierte Schlüsselwörter, um die Synchronisation sicherzustellen. Auf diese Weise kann durch die Verwendung eines konsistenten Sperrprotokolls zur Koordinierung des Zugriffs auf gemeinsam genutzten Zustand sichergestellt werden, dass unabhängig davon, welcher Thread die Sperre von gemeinsam genutzten Variablen hält, eine exklusive Methode zum Zugriff auf diese Variablen verwendet. Dies ist eine Art exklusives Schloss. Exklusives Schloss ist eigentlich eine Art pessimistisches Schloss, daher kann gesagt werden, dass synchronisiert ein pessimistisches Schloss ist.
Der pessimistische Sperrmechanismus hat die folgenden Probleme:
1. Unter Multi-Thread-Wettbewerb führt das Hinzufügen und Verlassen von Schlösser zu mehr Kontextumschalt- und Planungsverzögerungen, was zu Leistungsproblemen führt.
2. Ein Faden, der ein Schloss hält, bewirkt alle anderen Fäden, für die dieses Schloss hängen muss.
3. Wenn ein Thread mit hoher Priorität auf einen Thread mit niedriger Priorität wartet, um das Schloss freizusetzen, verursacht er Prioritätsumkehr, was zu Leistungsrisiken führt.
Im Vergleich zu diesen Problemen pessimistischer Schlösser sind eine weitere effektivere Sperrung optimistische Schlösser. Tatsächlich ist optimistisches Sperren: Jedes Mal, wenn Sie kein Schloss hinzufügen, aber Sie eine Operation abschließen, vorausgesetzt, es gibt keinen gleichzeitigen Konflikt. Wenn der gleichzeitige Konflikt fehlschlägt, versuchen Sie es erneut, bis er erfolgreich ist.
Optimistisches Schloss:
Optimistisches Sperren wurde oben erwähnt, aber es ist tatsächlich eine Art Gedanken. Im Vergleich zu pessimistischen Schlössern gehen optimistische Sperren aus, dass Daten im Allgemeinen keine gleichzeitigen Konflikte verursachen. Wenn die Daten eingereicht und aktualisiert werden, werden sie offiziell feststellen, ob die Daten über gleichzeitige Konflikte verfügen. Wenn ein gleichzeitiger Konflikt gefunden wird, werden die falschen Informationen des Benutzers zurückgegeben und der Benutzer entscheidet, wie dies zu tun ist.
Das oben erwähnte Konzept der optimistischen Sperre hat tatsächlich seine spezifischen Implementierungsdetails erläutert: Es enthält hauptsächlich zwei Schritte: Konflikterkennung und Datenaktualisierung. Eine der typischen Implementierungsmethoden ist vergleichen und tauschen (CAS).
CAS:
CAS ist eine optimistische Verriegelungstechnologie. Wenn mehrere Threads versuchen, CAS zu verwenden, um gleichzeitig dieselbe Variable zu aktualisieren, kann nur einer der Threads den Wert der Variablen aktualisieren, während die anderen Threads fehlschlagen. Der fehlgeschlagene Thread wird nicht suspendiert, wird jedoch mitgeteilt, dass dieser Wettbewerb gescheitert ist und es erneut versuchen kann.
Die CAS -Operation enthält drei Operanden - den Speicherort (v), der gelesen und geschrieben werden muss, der erwartete Originalwert (a) zum Vergleich und der neue Wert (b) zu schreiben. Wenn der Wert der Speicherposition v mit dem erwarteten Originalwert A übereinstimmt, aktualisiert der Prozessor den Positionswert automatisch auf den neuen Wert B B. Andernfalls wird der Prozessor nichts tun. In beiden Fällen wird der Wert dieses Standorts vor der CAS -Anweisung zurückgegeben. (In einigen besonderen Fällen von CAS, nur ob CAS erfolgreich ist oder nicht, ohne den aktuellen Wert zu extrahieren.) CAS stellt effektiv fest, dass "Position V den Wert A enthalten sollte; wenn er enthält, B in diese Position einfügen; ansonsten ändern Sie die Position nicht, sagen Sie mir einfach den aktuellen Wert dieser Position." Dies ist tatsächlich das gleiche wie das Prinzip der Konfliktprüfung + Datenaktualisierung optimistischer Sperren.
Lassen Sie mich hier betonen, dass optimistisches Sperren eine Art Gedanken ist. CAS ist eine Möglichkeit, diese Idee zu verwirklichen.
Java -Unterstützung für CAS:
Der neue java.util.concurrent (JUC) in JDK1.5 ist auf CAS erbaut. Im Vergleich zu blockierenden Algorithmen wie synchronisiert ist CAS eine häufige Implementierung von nicht blockierenden Algorithmen. Daher hat JUC seine Leistung erheblich verbessert.
Nehmen Sie Atomicinterger in java.util.concurrent als Beispiel, um zu sehen, wie die Sicherheit der Gewinde ohne Verwendung von Schlösser gewährleistet wird. Wir verstehen hauptsächlich die GetAndincrement -Methode, die der ++ I -Operation entspricht.
öffentliche Klasse AtomicInteger erweitert die Zahl implementiert Java.io.Serializable {private volatile int -Wert; public Final int get () {Rückgabewert; } public Final int getandIncrement () {für (;;) {int current = get (); int next = current + 1; if (vergleicheSet (aktuell, nächst) zurücksend; }} public Final Boolean Vergleiche (int erwart, int update) {return unafe.comPareAndswapint (this, ValueOffset, erwarten, update); }}Im Mechanismus ohne Schlösser muss der Feldwert verwendet werden, um sicherzustellen, dass die Daten zwischen Threads Sichtbarkeit sind. Auf diese Weise können Sie direkt lesen, wenn Sie den Wert einer Variablen erhalten. Dann mal sehen, wie ++ ich bin.
GetAndIncrement verwendet den CAS -Betrieb, und jedes Mal, wenn Sie Daten aus dem Speicher lesen und dann den CAS -Betrieb für diese Daten und das Ergebnis nach +1 ausführen. Wenn er erfolgreich ist, wird das Ergebnis zurückgegeben, sonst versuchen Sie es erneut, bis er erfolgreich ist.
VergleicheSet verwendet JNI (Java Native Interface), um den Betrieb von CPU -Anweisungen zu vervollständigen:
public Final Boolean VergleicheSet (int erwartet, int update) {return unafe.comPareAndswapint (this, valueOffset, erwarten, update); }wo unafe.comPareAndswapint (dies, ValueOffset, erwarten, aktualisieren); ähnelt der folgenden Logik:
if (this == erwarten) {this = update return true; } else {return false; }Wie kann ich dies vergleichen == erwarten, diese = update ersetzen, vergleiche, um die Atomizität dieser beiden Schritte zu erreichen? Beziehen Sie sich auf die Prinzipien von CAS
CAS -Prinzip:
CAS wird implementiert, indem JNI -Code angerufen wird. Vergleichswapint wird durch Verwendung von C implementiert, um die zugrunde liegenden CPU -Anweisungen aufzurufen.
Das Folgende erklärt das Implementierungsprinzip von CAS aus der Analyse der häufiger verwendeten CPU (Intel x86).
Hier ist der Quellcode der Vergleichswapt () -Methode der Sun.misc.unsafe -Klasse:
Public Final Native Boolean VergleicheSwapint (Objekt O, Long Offset, int erwartet, int x);
Sie können sehen, dass dies ein lokaler Methodenaufruf ist. Der C ++ - Code, den diese lokale Methode im JDK aufruft, lautet:
#define LOCK_IF_MP(mp) __asm cmmp mp, 0 / __asm je L0 / __asm _emit 0xF0 / __asm L0:inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { // alternative for InterlockedCompareExchange int mp = os :: is_mp (); __asm {mov edx, dest mov ecx, Exchange_value mov eax, compare_value lock_if_mp (mp) cmmpxchg dword ptr [edx], ecx}}Wie im obigen Quellcode gezeigt, entscheidet das Programm, ob der CMMPXCHG -Befehlsanweisung basierend auf dem Typ des aktuellen Prozessors ein Sperrvorfix hinzugefügt werden soll. Wenn das Programm auf einem Multiprozessor ausgeführt wird, fügen Sie das Sperrenpräfix zum CMMPXCHG -Befehl hinzu. Im Gegenteil, wenn das Programm auf einem einzelnen Prozessor ausgeführt wird, wird das Sperrenpräfix weggelassen (der einzelne Prozessor selbst behält die sequentielle Konsistenz innerhalb des Einzelprozessors bei und benötigt nicht den vom Vorfix des Sperrsperrung bereitgestellten Speicherbarriereffekts).
CAS -Nachteile:
1. ABA Fragen:
Wenn beispielsweise ein Thread eine aus der Speicherposition V ausnimmt, nimmt ein weiterer Thread zwei auch ein aus dem Speicher heraus, und zwei führen einige Operationen aus und werden B werden. Dann dreht zwei die Daten an v -Position A. Zu diesem Zeitpunkt führt ein Thread eine CAS -Operation aus und stellt fest, dass A immer noch im Speicher ist, und einer arbeitet dann erfolgreich. Obwohl der CAS -Betrieb von Thread One erfolgreich ist, kann es versteckte Probleme geben. Wie unten gezeigt:
Es gibt einen Stapel mit einer Einweg-verlinkten Liste, wobei die Spitze des Stacks A ist. Zu diesem Zeitpunkt weiß Tim T1 bereits, dass A.Next B ist, und hofft dann, die Oberseite des Stapels durch B durch C CAS zu ersetzen:
head.comPareandset (a, b);
Bevor T1 die obige Anweisung ausführt, steckt T2 T2 ein, stellt A und B aus dem Stapel und dann ist die Stapelstruktur wie folgt, und das Objekt B befindet sich zu diesem Zeitpunkt in einem freien Zustand:
Zu diesem Zeitpunkt ist es an der Reihe von Thread T1, den CAS -Betrieb durchzuführen. Die Erkennung ergab, dass die Spitze des Stapels immer noch A ist, also ist CAS erfolgreich und die Spitze des Stapels wird B, aber tatsächlich ist BNEXT null, so dass die Situation zu diesem Zeitpunkt:
Es gibt nur ein Element B im Stapel, und die verknüpfte Liste, die aus C und D besteht, existiert nicht mehr im Stapel. C und D werden ohne Grund weggeworfen.
Ab Java 1.5 bietet das Atomic -Paket des JDK eine KlassenatomicStampedReference, um das ABA -Problem zu lösen. Die Vergleichs -Methode dieser Klasse besteht darin, zuerst zu überprüfen, ob die aktuelle Referenz der erwarteten Referenz entspricht und ob das aktuelle Flag dem erwarteten Flag entspricht. Wenn alle gleich sind, werden die Referenz und der Wert des Flags auf den angegebenen aktualisierten Wert auf atomare Weise eingestellt.
Public Boolean VergleicheSet (V erwartungsgemäß, // erwartete Referenz gegen NewReference, // aktualisierte Referenz int erwartungsgemäß
Tatsächlicher Anwendungscode:
private statische AtomicStampedReference <Ganzzahl> atomicStampedref = neuer AtomicStampedReference <Ganzzahl> (100, 0); ......... atomicStampedref.comPareAndset (100, 101, Stempel, Stempel + 1);
2. Langzykluszeit und hoher Overhead:
Spin CAS (wenn es fehlschlägt, wird es bis zum Erfolg von Fahrrad ausgeführt) Wenn es für lange Zeit fehlschlägt, bringt es die CPU mit großer Ausführung über. Wenn die JVM die vom Prozessor bereitgestellten Pauseanweisungen unterstützen kann, wird die Effizienz bis zu einem gewissen Grad verbessert. Die Pause -Anweisungen haben zwei Funktionen. Erstens kann es die Ausführungsanweisungen (Depipeline) verzögern, damit die CPU nicht zu viel Ausführungsressourcen verbraucht. Die Verzögerungszeit hängt von der spezifischen Implementierungsversion ab. Bei einigen Prozessoren ist die Verzögerungszeit Null. Zweitens kann die CPU -Pipeline -Spülung durch Verstöße gegen den Speicherauftrag beim Verlassen der Schleife vermieden werden, wodurch die CPU -Ausführungseffizienz verbessert wird.
3. Es kann nur atomare Operationen einer gemeinsam genutzten Variablen garantiert werden:
Bei der Durchführung von Operationen in einer gemeinsam genutzten Variablen können wir die zyklische CAS -Methode verwenden, um atomare Operationen sicherzustellen. Beim Betrieb mehrerer gemeinsamer Variablen kann der zyklische CAS jedoch die Atomizität des Betriebs nicht garantieren. Zu diesem Zeitpunkt können Sie Sperren verwenden, oder es gibt einen Trick, der mehrere gemeinsam genutzte Variablen in eine gemeinsame Variable zusammenführen soll. Zum Beispiel gibt es zwei gemeinsame Variablen i = 2, j = a, merge ij = 2a und verwenden dann CAS, um IJ zu bedienen. Ab Java 1.5 stellt JDK die Atomicreference -Klasse zur Verfügung, um die Atomizität zwischen referenzierten Objekten zu gewährleisten. Sie können mehrere Variablen in ein Objekt für den CAS -Betrieb einfügen.
CAS- und synchronisierte Nutzungsszenarien:
1. In Situationen, in denen weniger Ressourcenwettbewerb (Light-Thread-Konflikt) vorhanden ist, ist die Verwendung der synchronisierten Synchronisationsschloss für das Blockieren von Threads und das Weckschalt- und Switching-Vorgänge zwischen den Kernelzuständen des Benutzerzustands eine zusätzliche Verschwendung von CPU-Ressourcen. Während CAS basierend auf Hardware implementiert ist, muss nicht in den Kernel gelangen, muss keine Threads wechseln, und die Wahrscheinlichkeit, dass die Betriebsspins betrieben werden, ist geringer, sodass eine höhere Leistung erzielt werden kann.
2. In Situationen, in denen der Ressourcenwettbewerb schwerwiegend ist (schwerer Fadenkonflikt), ist die Wahrscheinlichkeit eines CAS -Spin relativ hoch, was mehr CPU -Ressourcen verschwendet und weniger effizient als synchronisiert ist.
Ergänzung: Synchronisiert wurde nach JDK1.6 verbessert und optimiert. Die zugrunde liegende Implementierung von Synchronisierten hängt hauptsächlich auf der Warteschlange der lock-freien ab. Die Grundidee besteht darin, nach dem Spin zu blockieren, nach dem Wechsel des Wettbewerbs weiterhin um Schlösser zu kämpfen, die Fairness leicht zu opfern, aber einen hohen Durchsatz erhalten. Wenn weniger Fadenkonflikte vorhanden sind, kann eine ähnliche Leistung erzielt werden. Wenn es ernsthafte Fadenkonflikte gibt, ist die Leistung viel höher als die von CAS.
Implementierung des gleichzeitigen Pakets:
Da Javas CAS beide Speichersemantik für volatiles Lesen und volatiles Schreiben hat, gibt es jetzt vier Möglichkeiten, zwischen Java -Threads zu kommunizieren:
1. Faden A schreibt die flüchtige Variable und dann liest B die flüchtige Variable.
2. Thread A schreibt die flüchtige Variable, und dann verwendet Thread B CAS, um die flüchtige Variable zu aktualisieren.
3. Thread A verwendet CAS, um eine flüchtige Variable zu aktualisieren, und dann verwendet Thread B CAS, um diese volatile Variable zu aktualisieren.
4. Thread A verwendet CAS, um eine flüchtige Variable zu aktualisieren, und dann liest sich die flüchtige Variable B.
Java's CAS verwendet effiziente Atomanweisungen auf maschinelle Ebene auf moderne Prozessoren, die atomisch von Lesewechselschreiber bei Speichervorgängen durchgeführt werden. Dies ist der Schlüssel zum Erreichen der Synchronisation in Multiprozessoren (im Wesentlichen eine Computermaschine, die Atom-Read-Change-Writ-Anweisungen unterstützt, die mit dem asynchronen Gäste-Maschinen ein paar atomices-maschulierende Maschinen berechnen können, die sequentiell-kalkulierende Turting-Maschinen, so dass alle modernen Anweisungen, so wie es mit modernen Machzita-Anweisungen berechnet werden, und alle modernen Anweisungen, so dass alle modernen Anweisungen, so wie es mit modernen Machzeilen berechnet werden, und alle modernen Anweisungen, so wie es mit modalem und komischem Maschinen berechnet werden, und alle modernen Anweisungen, so dass es sich mit modalem Machtzugs handelt. Dies kann atomare Operationen für den Messumwandlungsschreiben im Speicher ausführen. Gleichzeitig können das Lese-/Schreib- und CAS der flüchtigen Variablen die Kommunikation zwischen Threads realisieren. Das Zusammenstellen dieser Merkmale bildet den Eckpfeiler der Implementierung des gesamten gleichzeitigen Pakets. Wenn wir die Quellcode -Implementierung des gleichzeitigen Pakets sorgfältig analysieren, finden wir ein allgemeines Implementierungsmuster:
1. Deklarieren Sie zunächst die gemeinsame Variable als volatil;
2. Verwenden Sie dann das Atombedingungs -Update von CAS, um die Synchronisation zwischen Threads zu erreichen.
3. Gleichzeitig wird die Kommunikation zwischen Threads durch die Verwendung des Lese-/Schreibens von flüchtigem und der Speichersemantik des flüchtigen Lesens und Schreibens in CAS erreicht.
AQs, nicht blockierende Datenstrukturen und Atomvariablenklassen (Klassen in der java.util.concurrent.atomic-Paket) werden die grundlegenden Klassen in diesen gleichzeitigen Paketen mit diesem Muster implementiert, und die hochrangigen Klassen im gleichzeitigen Paket stützen sich auf diese grundlegenden Klassen, um sie zu implementieren. Aus allgemeiner Sicht lautet das Implementierungsdiagramm des gleichzeitigen Pakets wie folgt:
CAS (Zuordnung von Objekten im Haufen):
Java ruft new object() auf, um ein Objekt zu erstellen, das dem JVM -Haufen zugewiesen wird. Wie wird dieses Objekt auf dem Haufen gespeichert?
Erstens, wenn new object() ausgeführt wird, wie viel Platz dieser Objekt tatsächlich bestimmt wird, da die verschiedenen Datentypen in Java und wie viel Platz sie einnehmen (wenn Sie nicht über das Prinzip klar sind, googeln Sie es selbst). Dann besteht die nächste Aufgabe darin, ein Stück Platz im Haufen zu finden, um dieses Objekt zu speichern.
Im Fall eines einzelnen Threads gibt es im Allgemeinen zwei Zuordnungsstrategien:
1. Zeigerkollision: Dies gilt im Allgemeinen für den Speicher, der absolut regelmäßig ist (ob der Speicher regelmäßig von der Speicherrecyclingstrategie abhängt). Die Aufgabe, Platz zuzuweisen, besteht darin, den Zeiger wie die Entfernung der Objektgröße auf der Seite des freien Speichers zu bewegen.
2. Kostenlose Liste: Dies ist für den nicht regulären Speicher geeignet. In diesem Fall führt der JVM eine Speicherliste, um aufzuzeichnen, welche Speicherbereiche kostenlos sind und welche Größe. Wenn Sie Objekte Platz zuweisen, gehen Sie zur freien Liste, um den entsprechenden Bereich abzufragen und dann zuzuordnen.
Es ist jedoch unmöglich, dass der JVM ständig in einem einzigen Fadenzustand läuft, sodass die Effizienz zu schlecht ist. Da es sich bei der Zuweisung von Speicher an ein anderes Objekt nicht um einen Atomoperation handelt, sind zumindest die folgenden Schritte erforderlich: Finden einer kostenlosen Liste, Zuweisung von Speicher, Änderungen einer kostenlosen Liste usw., was nicht sicher ist. Es gibt auch zwei Strategien, um die Sicherheitsprobleme während der Parallelität zu lösen:
1. CAS: Tatsächlich verwendet die virtuelle Maschine CAS, um die Atomizität des Aktualisierungsvorgangs zu gewährleisten, indem es nicht wiederholt wird, und das Prinzip ist das gleiche wie oben erwähnt.
2. TLAB: Wenn CAS verwendet wird, wirkt sich dies tatsächlich auf die Leistung aus, sodass die JVM eine fortschrittlichere Optimierungsstrategie vorgeschlagen hat: Jeder Thread eilt ein kleines Stück Speicher im Java-Heap vor, der als lokaler Thread Allocation Puffer (TLAB) bezeichnet wird. Wenn der Thread den Speicher darin zuordnen muss, reicht er aus, um ihn direkt auf TLAB zuzuweisen und Thread -Konflikte zu vermeiden. Erst wenn der Pufferspeicher aufgebraucht wird und der Speicher für den CAS -Vorgang zur Zuordnung eines größeren Speicherplatzes ausführt.
Ob die virtuelle Maschine TLAB verwendet, kann über -XX:+/-UseTLAB konfiguriert werden (JDK5 und spätere Versionen sind standardmäßig aktiviert).
Das obige ist der gesamte Inhalt dieses Artikels. Ich hoffe, es wird für das Lernen aller hilfreich sein und ich hoffe, jeder wird Wulin.com mehr unterstützen.