Lesen wir zuerst eine detaillierte Erklärung von Synchronized:
Synchronisiert ist ein Schlüsselwort in der Java -Sprache. Wenn es verwendet wird, um eine Methode oder einen Codeblock zu ändern, kann sichergestellt werden, dass höchstens ein Thread den Code gleichzeitig ausführt.
1. Wenn zwei gleichzeitige Threads auf diesen synchronisierten (this) synchronisierten Codeblock in demselben Objektobjekt zugreifen, kann nur ein Thread innerhalb eines Zeitpunkts ausgeführt werden. Ein weiterer Thread muss warten, bis der aktuelle Thread diesen Codeblock ausführt, bevor er den Codeblock ausführen kann.
2. Wenn jedoch ein Thread auf einen synchronisierten (this) -Synchronisationscode-Codeblock eines Objekts zugreift, kann ein anderer Thread weiterhin auf den nicht synchronisierten (this) Synchronisationscodeblock in diesem Objekt zugreifen.
3.. Es ist besonders wichtig, dass bei einem Thread auf einen synchronisierten (this) -Synchronisationscodeblock eines Objekts zugreift, andere Threads aus dem Zugriff auf alle anderen synchronisierten Synchronisationscodeblöcke im Objekt blockiert werden.
4. Das dritte Beispiel gilt auch für andere Synchroncodeblöcke. Das heißt, wenn ein Thread auf einen synchronisierten (this) -Synchronisationscode -Codeblock eines Objekts zugreift, wird die Objektsperrung dieses Objekts erhalten. Infolgedessen werden andere Threads auf alle synchronen Code -Teile des Objektobjekts zugreifen.
5. Die oben genannten Regeln gelten auch für andere Objektschlösser.
Einfach ausgedrückt, synchronisiert wird ein Schloss für den aktuellen Thread deklariert. Der Thread mit dieser Schloss kann Anweisungen im Block ausführen, und andere Threads können nur darauf warten, dass die Sperre vor derselben Operation erfasst.
Das ist sehr nützlich, aber ich habe eine andere seltsame Situation begegnet.
1. In derselben Klasse gibt es zwei Methoden: Verwenden der synchronisierten Schlüsselwortdeklaration
2. Wenn Sie eine der Methoden ausführen, müssen Sie darauf warten, dass die andere Methode (asynchroner Thread -Rückruf) ausgeführt wird, damit Sie eine Countdownlatch zum Warten verwenden.
3. Der Code ist wie folgt dekonstruiert:
synchronisierte void a () {countdownlatch = new Countdownlatch (1); // MEHLE COUNDDOWNLATCH.AWAIT ();} Synchronisierte void b () {Countdownlatch.Countdown ();}; In
Die Methode A wird vom Haupt -Thread ausgeführt, Methode B wird vom asynchronen Thread ausgeführt und das Ergebnis der Rückrufausführung lautet:
Der Haupt -Thread beginnt nach der Ausführung der A -Methode stecken zu bleiben, und es wird nicht mehr, und es wird für Sie nutzlos sein, auf zu warten, egal wie lange es dauert.
Dies ist ein klassisches Deadlock -Problem
A wartet darauf, dass B ausgeführt wird, aber tatsächlich glauben Sie nicht, dass B ein Rückruf ist, B wartet auch darauf, dass A ausgeführt werden kann. Warum? Synchronisierte spielt eine Rolle.
Wenn wir einen Codeblock synchronisieren möchten, müssen wir im Allgemeinen eine gemeinsam genutzte Variable verwenden, um ihn zu sperren, zum Beispiel:
byte [] mutex = new Byte [0];
Wenn der Inhalt der A -Methode und die B -Methode in die synchronisierten Blöcke der A1- und B1 -Methoden migriert wird, ist es leicht zu verstehen.
Nach der Ausführung von A1 wartet es indirekt auf die Ausführung der (Countdownlatch) B1 -Methode.
Da der Mutex in A1 jedoch nicht veröffentlicht wird, warten wir auf B1. Zu diesem Zeitpunkt muss die B -Methode nicht ausgeführt werden, wenn die asynchrone Rückruf -B1 -Methode auf die Freigabe der Sperre des Blocks warten muss.
Dies verursachte einen Deadlock!
Das synchronisierte Schlüsselwort hier wird vor der Methode platziert, und die Funktion ist gleich. Es ist nur so, dass die Java -Sprache Ihnen hilft, die Erklärung und Verwendung von Mutex zu verbergen. Die im selben Objekt verwendete synchronisierte Methode ist gleich, sodass selbst ein asynchroner Rückruf zu Deadlocks führt. Achten Sie also auf dieses Problem. Diese Fehlerebene ist, dass das synchronisierte Schlüsselwort nicht ordnungsgemäß verwendet wird. Verwenden Sie es nicht zufällig und verwenden Sie es richtig.
Was genau ist ein so unsichtbares Mutex -Objekt?
Das Beispiel selbst ist leicht zu denken. Denn auf diese Weise müssen kein neues Objekt definiert und ein Schloss hergestellt werden. Um diese Idee zu beweisen, können Sie ein Programm schreiben, um es zu beweisen.
Die Idee ist sehr einfach. Definieren Sie eine Klasse und es gibt zwei Methoden. Der eine wird synchronisiert und der andere wird in der Methodenkörper synchronisiert (dieses) verwendet. Starten Sie dann zwei Threads, um diese beiden Methoden separat aufzurufen. Wenn zwischen den beiden Methoden (Warten) der Schlosswettbewerb auftritt, kann erklärt werden, dass der unsichtbare Mutex in synchronisiertem, der nach der Methode deklariert wurde, tatsächlich die Instanz selbst ist.
öffentliche Klasse multitHeheadsync {public synchronisierte void m1 () löst InterruptedException {System aus. out.println ("m1 call"); Faden. Schlaf (2000); System. out.println ("M1 Call Done"); } public void m2 () löst InterruptedException {synchronisiert (this) {System. out.println ("m2 call"); Faden. Schlaf (2000); System. out.println ("M2 Call Done"); }} public static void main (String [] args) {endgültig multithreadsync thisObj = new MultitHeheadsync (); Thread t1 = neuer Thread () {@Override public void run () {try {thisObj.m1 (); } catch (interruptedException e) {e.printstacktrace (); }}}}; Thread t2 = neuer Thread () {@Override public void run () {try {thisObj.m2 (); } catch (interruptedException e) {e.printstacktrace (); }}}; t1.start (); t2.Start (); }} Die Ergebnisausgabe ist:
M1 CALLM1 Rufen Sie Donem2 Callm2 Anruf ab
Es wird erklärt, dass der Synchronisierungsblock der Methode M2 auf die Ausführung von M1 wartet. Dies kann das obige Konzept bestätigen.
Es ist zu beachten, dass das gesperrte Objekt die Klasseninstanz der aktuellen Klasse ist, wenn die Synchronisierung zu der statischen Methode hinzugefügt wird. Sie können auch ein Programm schreiben, um es zu beweisen. Hier wird es weggelassen.
Daher kann das synchronisierte Schlüsselwort der Methode beim Lesen automatisch durch synchronisiert (dieses) {} ersetzt werden, was leicht zu verstehen ist.
void method () {void synchronisierte Methode () {synchronisiert (this) {// Biz-Code // Biz-Code} ------ >>>}} Speichersichtbarkeit von synchronisiert
In Java wissen wir alle, dass das Keyword -synchronisierte Keyword verwendet werden kann, um den gegenseitigen Ausschluss zwischen Threads zu implementieren, aber wir vergessen oft, dass es eine andere Funktion hat, dh, um die Sichtbarkeit von Variablen im Speicher zu gewährleisten - das heißt, wenn zwei Threads gelesen werden und den gleichen Variablen zum gleichen Variablen zum gleichen Zeitpunkt geschrieben haben.
Zum Beispiel das folgende Beispiel:
öffentliche Klasse Novisibilität {privat statischer Boolean Ready = False; private statische Int -Nummer = 0; private statische Klasse readerthread erweitert Thread {@Override public void run () {while (! Ready) {thread.yield (); // Unterstützen Sie die CPU, um andere Threads funktionieren zu lassen} System.out.println (Nummer); }} public static void main (String [] args) {new readerthread (). start (); Zahl = 42; bereit = wahr; }}Was wird Ihrer Meinung nach das Lesen von Threads ausgeben? 42? Unter normalen Umständen wird 42 ausgegeben. Aufgrund von Nachbestellenproblemen kann der Lese -Thread jedoch 0 ausgeben oder nichts ausgeben.
Wir wissen, dass der Compiler den Code beim Kompilieren von Java -Code in Bytecode neu ordnen kann, und die CPU kann seine Anweisungen auch bei der Ausführung von Maschinenanweisungen neu ordnen. Solange die Neuordnung die Semantik des Programms nicht zerstört
In einem einzigen Thread kann es nicht garantiert werden, dass die im Programm angegebenen Reihenfolge nicht garantiert werden müssen, dass die Operationen in der Reihenfolge ausgeführt werden müssen, selbst wenn die Neuordnung möglicherweise erhebliche Auswirkungen auf andere Threads haben.
Dies bedeutet, dass die Ausführung der Anweisung "Ready = True" Vorrang vor der Ausführung der Anweisung "Nummer = 42" haben kann. In diesem Fall kann der Lese -Thread den Standardwert von Nummer 0 ausgeben.
Nach dem Java -Speichermodell führt Neubestehen von Problemen zu solchen Problemen der Sichtbarkeit von Speicher. Unter dem Java -Speichermodell verfügt jeder Thread über seinen eigenen Arbeitsspeicher (hauptsächlich den CPU -Cache oder das Register), und seine Vorgänge auf Variablen werden in seinem eigenen Arbeitsspeicher durchgeführt, während die Kommunikation zwischen Threads durch Synchronisation zwischen Hauptspeicher und Thread -Arbeitsspeicher erreicht wird.
Zum Beispiel zum obigen Beispiel hat der Schreib Thread die Zahl erfolgreich auf 42 aktualisiert und bereit für True bereit, aber es ist sehr wahrscheinlich, dass der Schreib Thread nur die Zahl mit dem Hauptspeicher synchronisiert (möglicherweise aufgrund des Schreibpuffers der CPU), was dazu führt, dass die von den nachfolgenden gelesenen Threads gelesenen Ready -Wert immer falsch sind, sodass der obige Code keine numerischen Werte ausgibt.
Wenn wir das synchronisierte Schlüsselwort verwenden, um zu synchronisieren, wird es kein solches Problem geben.
öffentliche Klasse Novisibilität {privat statischer Boolean Ready = False; private statische Int -Nummer = 0; private statische Objekt lock = new Object (); private statische Klasse ReaderThread erweitert Thread {@Override public void run () {synchronized (lock) {while (! Ready) {thread.yield (); } System.out.println (Nummer); }} public static void main (String [] args) {synchronized (lock) {new readerthread (). start (); Zahl = 42; bereit = wahr; }}} Dies liegt daran, dass das Java -Speichermodell die folgenden Garantien für die synchronisierte Semantik bietet.
Das heißt, wenn Threada Sperre M veröffentlichen, werden die Variablen, die es geschrieben hat (z. B. x und y, die in seinem Arbeitsspeicher vorhanden sind), mit dem Hauptspeicher synchronisiert. Wenn das Taste für dieselbe Sperre M gilt, wird der Arbeitsspeicher von Thread auf ungültig gesetzt. Anschließend lädt Thread die Variable, auf die sie aus dem Hauptspeicher in den Arbeitsspeicher zugreifen möchte (zu diesem Zeitpunkt, x = 1, y = 1, der neueste in Threada modifizierte Wert). Auf diese Weise wird die Kommunikation zwischen Threads von Threada und Faden erreicht.
Dies ist tatsächlich eine der von JSR133 definierten Regeln. JSR133 definiert den folgenden Satz von Vorschriften für das Java-Speichermodell.
Tatsächlich definiert dieser Satz von Vorfordern die Speichersichtbarkeit zwischen Operationen. Wenn ein Betrieb vor B-Vor Operationen stattfindet, muss das Ausführungsergebnis einer Operation (z. B. Schreiben in Variablen) bei der Durchführung des B-Betriebs sichtbar sein.
Nehmen wir ein Beispiel:
// Code, der durch Thread A und B Object Lock = New Object (); int a = 0; int b = 0; int c = 0; // Thread A, den folgenden Code Synchronized (Lock) {a = 1; // 1 b = 2; // 2} // 3c = 3; // 4 // Thread B, rufen Sie den folgenden Code synchronisiert (Sperre) {// 5 System.out.println (a); // 6 system.out.println (b); // 7 system.out.println (c); // 8}Wir gehen davon aus, dass Thread A zuerst ausgeführt wird, den drei Variablen A, B und C Werte zuweist (Hinweis: Die Zuordnung von Variablen A, B wird im synchronen Anweisungsblock durchgeführt) und dann wird Thread B erneut ausgeführt, wodurch die Werte dieser drei Variablen gelesen und ausgedruckt werden. Was sind die Werte der Variablen A, B und C von Thread B ausgedruckt?
Gemäß der Einzel-Threading-Regel können wir bei der Ausführung von Thread A erhalten, dass ein Betrieb vor 2 Vorgängen stattfindet, 2 Vorgänge vor 3 Vorgängen und 3 Vorgänge vor 4 Vorgängen erfolgen. In ähnlicher Weise erfolgen bei der Ausführung von Thread B 5 Vorgänge vor 6 Vorgängen, 6 Vorgänge vor 7 Vorgängen und 7 Vorgänge vor 8 Operationen. Nach den Prinzipien der Entsperren und Verriegelung des Monitors erfolgt die 3 Operationen (Entsperrung) vor 5 Vorgängen (Verriegelungsbetrieb). Nach den Übergangsregeln können wir zu dem Schluss kommen, dass die Operationen 1 und 2 vor den Operationen 6, 7 und 8 erfolgen.
Gemäß der Speichersemantik von passiert-vor Ort sind die Ausführungsergebnisse der Operationen 1 und 2 für Operationen 6, 7 und 8 sichtbar, sodass in Thread B, A und B gedruckt werden, 1 und 2. für Operationen 4 und Operation 8 der Variablen c. Wir können den Vorgang 4 nicht vor Operation 8 nach den bestehenden Vorschriften vor den Regeln abschließen. Daher kann in Thread B die Variable, auf die C auf C zugegriffen wird, immer noch 0 sein, nicht 3.