Was ist der Unterschied zwischen atomar / flüchtig / synchronisiert?

297

Wie funktioniert atomar / flüchtig / synchronisiert intern?

Was ist der Unterschied zwischen den folgenden Codeblöcken?

Code 1

private int counter;

public int getNextUniqueIndex() {
    return counter++; 
}

Code 2

private AtomicInteger counter;

public int getNextUniqueIndex() {
    return counter.getAndIncrement();
}

Code 3

private volatile int counter;

public int getNextUniqueIndex() {
    return counter++; 
}

Funktioniert es volatilefolgendermaßen? Ist

volatile int i = 0;
void incIBy5() {
    i += 5;
}

gleichwertig

Integer i = 5;
void incIBy5() {
    int temp;
    synchronized(i) { temp = i }
    synchronized(i) { i = temp + 5 }
}

Ich denke, dass zwei Threads nicht gleichzeitig in einen synchronisierten Block eintreten können ... habe ich recht? Wenn dies zutrifft, wie funktioniert es atomic.incrementAndGet()dann ohne synchronized? Und ist es threadsicher?

Und was ist der Unterschied zwischen internem Lesen und Schreiben in flüchtige Variablen / atomare Variablen? Ich habe in einem Artikel gelesen, dass der Thread eine lokale Kopie der Variablen hat - was ist das?

Hardik
quelle
5
Das wirft eine Menge Fragen auf, mit Code, der nicht einmal kompiliert wird. Vielleicht sollten Sie ein gutes Buch wie Java Concurrency in Practice lesen.
JB Nizet
4
@JBNizet du hast recht !!! Ich habe dieses Buch, es hat kein Atomic-Konzept in Kürze und ich bekomme keine Konzepte davon. vom Fluch ist es mein Fehler nicht vom Autor.
Hardik
4
Sie müssen sich nicht wirklich darum kümmern, wie es implementiert ist (und es variiert mit dem Betriebssystem). Was Sie verstehen müssen, ist der Vertrag: Der Wert wird atomar erhöht, und alle anderen Threads sehen garantiert den neuen Wert.
JB Nizet

Antworten:

392

Sie fragen speziell, wie sie intern funktionieren . Hier sind Sie also:

Keine Synchronisation

private int counter;

public int getNextUniqueIndex() {
  return counter++; 
}

Es liest im Grunde genommen Werte aus dem Speicher, erhöht sie und speichert sie wieder im Speicher. Dies funktioniert in einem einzelnen Thread, aber heutzutage funktioniert es im Zeitalter von Multi-Core-, Multi-CPU- und Multi-Level-Caches nicht mehr richtig. Zunächst wird die Race-Bedingung (mehrere Threads können den Wert gleichzeitig lesen), aber auch Sichtbarkeitsprobleme eingeführt. Der Wert wird möglicherweise nur im " lokalen " CPU-Speicher (etwas Cache) gespeichert und ist für andere CPUs / Kerne (und damit - Threads) nicht sichtbar. Aus diesem Grund beziehen sich viele auf die lokale Kopie einer Variablen in einem Thread. Es ist sehr unsicher. Betrachten Sie diesen beliebten, aber fehlerhaften Code zum Stoppen von Threads:

private boolean stopped;

public void run() {
    while(!stopped) {
        //do some work
    }
}

public void pleaseStop() {
    stopped = true;
}

volatileZur stoppedVariablen hinzufügen und es funktioniert einwandfrei - wenn ein anderer Thread geändert wirdstopped Variable über eine pleaseStop()Methode , wird diese Änderung garantiert sofort in der while(!stopped)Schleife des Arbeitsthreads angezeigt . Übrigens ist dies auch keine gute Möglichkeit, einen Thread zu unterbrechen. Weitere Informationen finden Sie unter: So stoppen Sie einen Thread, der für immer ohne Verwendung ausgeführt wird, und Stoppen eines bestimmten Java-Threads .

AtomicInteger

private AtomicInteger counter = new AtomicInteger();

public int getNextUniqueIndex() {
  return counter.getAndIncrement();
}

Die AtomicIntegerKlasse verwendet CAS -CPU-Operationen ( Compare-and-Swap ) auf niedriger Ebene (keine Synchronisierung erforderlich!). Mit ihnen können Sie eine bestimmte Variable nur ändern, wenn der aktuelle Wert gleich etwas anderem ist (und erfolgreich zurückgegeben wird). Wenn Sie es also ausführen, wird getAndIncrement()es tatsächlich in einer Schleife ausgeführt (vereinfachte reale Implementierung):

int current;
do {
  current = get();
} while(!compareAndSet(current, current + 1));

Also im Grunde: lesen; Versuchen Sie, einen inkrementierten Wert zu speichern. Wenn dies nicht erfolgreich ist (der Wert ist nicht mehr gleich current), lesen Sie es und versuchen Sie es erneut. Das compareAndSet()ist in nativem Code (Assembly) implementiert.

volatile ohne Synchronisation

private volatile int counter;

public int getNextUniqueIndex() {
  return counter++; 
}

Dieser Code ist nicht korrekt. Es behebt das Sichtbarkeitsproblem ( volatilestellt sicher, dass andere Threads Änderungen sehen können, an denen Änderungen vorgenommen wurden counter), hat jedoch weiterhin eine Race-Bedingung. Dies wurde mehrfach erklärt : Pre / Post-Inkrementierung ist nicht atomar.

Die einzige Nebenwirkung von volatile ist das " Leeren " von Caches, sodass alle anderen Parteien die aktuellste Version der Daten sehen. Dies ist in den meisten Situationen zu streng. Deshalb volatileist nicht Standard.

volatile ohne Synchronisation (2)

volatile int i = 0;
void incIBy5() {
  i += 5;
}

Das gleiche Problem wie oben, aber noch schlimmer, weil i es nicht ist private. Die Rennbedingung ist noch vorhanden. Warum ist das ein Problem? Wenn beispielsweise zwei Threads diesen Code gleichzeitig ausführen, lautet die Ausgabe möglicherweise + 5oder + 10. Sie werden jedoch garantiert die Änderung sehen.

Mehrfach unabhängig synchronized

void incIBy5() {
  int temp;
  synchronized(i) { temp = i }
  synchronized(i) { i = temp + 5 }
}

Überraschung, dieser Code ist auch falsch. In der Tat ist es völlig falsch. Zunächst synchronisieren Sie auf i, was geändert werden soll (außerdem iist es ein Grundelement, also denke ich, dass Sie auf einer temporären Seite synchronisierenInteger das über Autoboxing erstellt wurde ...). Vollständig fehlerhaft. Sie könnten auch schreiben:

synchronized(new Object()) {
  //thread-safe, SRSLy?
}

Keine zwei Threads können denselben synchronizedBlock betreten mit derselben Sperre . In diesem Fall (und ähnlich in Ihrem Code) ändert sich das Sperrobjekt bei jeder Ausführungsynchronized effektiv keine Auswirkung.

Selbst wenn Sie eine endgültige Variable (oder this) für die Synchronisation verwendet haben, ist der Code immer noch falsch. Zwei Fäden können zuerst gelesen , ium tempsynchron (mit dem gleichen Wert in lokaltemp ), dann werden die ersten ordnet einen neuen Wert i(sagen wir von 1 bis 6) und die andere tut dasselbe (1 bis 6).

Die Synchronisation muss vom Lesen bis zum Zuweisen eines Wertes reichen. Ihre erste Synchronisation hat keine Auswirkung (das Lesen von intist atomar) und die zweite ebenfalls. Meiner Meinung nach sind dies die richtigen Formen:

void synchronized incIBy5() {
  i += 5 
}

void incIBy5() {
  synchronized(this) {
    i += 5 
  }
}

void incIBy5() {
  synchronized(this) {
    int temp = i;
    i = temp + 5;
  }
}
Tomasz Nurkiewicz
quelle
10
Das einzige, was ich hinzufügen möchte, ist, dass die JVM variable Werte in Register kopiert, um sie zu bearbeiten. Dies bedeutet, dass Threads, die auf einer einzelnen CPU / einem einzelnen Kern ausgeführt werden, immer noch unterschiedliche Werte für eine nichtflüchtige Variable sehen können.
David Harkness
@thomasz: ist compareAndSet (aktuell, aktuell + 1) synchronisiert ?? Wenn nein, was passiert, wenn zwei Threads diese Methode gleichzeitig ausführen?
Hardik
@Hardik: compareAndSetist nur ein dünner Wrapper um den CAS-Betrieb. Ich gehe in meiner Antwort auf einige Details ein.
Tomasz Nurkiewicz
1
@thomsasz: ok, ich gehe diese Link- Frage durch und beantworte sie von Jon Skeet. Er sagt: "Thread kann eine flüchtige Variable nicht lesen, ohne zu überprüfen, ob ein anderer Thread einen Schreibvorgang ausgeführt hat." aber was passiert, wenn sich ein Thread zwischen dem Schreibvorgang befindet und der zweite Thread ihn liest !! Liege ich falsch ?? ist es nicht Race Bedingung auf atomaren Betrieb?
Hardik
3
@Hardik: Bitte erstellen Sie eine weitere Frage, um mehr Antworten auf Ihre Fragen zu erhalten. Hier sind nur Sie und ich und Kommentare sind nicht geeignet, um Fragen zu stellen. Vergiss nicht, hier einen Link zu einer neuen Frage zu posten, damit ich nachverfolgen kann.
Tomasz Nurkiewicz
61

Wenn Sie eine Variable als flüchtig deklarieren, wirkt sich das Ändern ihres Werts sofort auf den tatsächlichen Speicher der Variablen aus. Der Compiler kann keine Verweise auf die Variable optimieren. Dies garantiert, dass alle anderen Threads den neuen Wert sofort sehen, wenn ein Thread die Variable ändert. (Dies ist für nichtflüchtige Variablen nicht garantiert.)

Das Deklarieren einer atomaren Variablen garantiert, dass Operationen, die an der Variablen ausgeführt werden, atomar ablaufen, dh dass alle Teilschritte der Operation innerhalb des Threads abgeschlossen sind, in dem sie ausgeführt werden, und nicht von anderen Threads unterbrochen werden. Beispielsweise erfordert eine Inkrementierungs- und Testoperation, dass die Variable inkrementiert und dann mit einem anderen Wert verglichen wird. Eine atomare Operation garantiert, dass beide Schritte so ausgeführt werden, als wären sie eine einzige unteilbare / unterbrechungsfreie Operation.

Durch das Synchronisieren aller Zugriffe auf eine Variable kann jeweils nur ein Thread auf die Variable zugreifen, und alle anderen Threads müssen warten, bis dieser Zugriffsthread seinen Zugriff auf die Variable freigibt.

Der synchronisierte Zugriff ähnelt dem atomaren Zugriff, die atomaren Operationen werden jedoch im Allgemeinen auf einer niedrigeren Programmierebene implementiert. Es ist auch durchaus möglich, nur einige Zugriffe auf eine Variable zu synchronisieren und zuzulassen, dass andere Zugriffe nicht synchronisiert werden (z. B. alle Schreibvorgänge in eine Variable synchronisieren, aber keinen der Lesevorgänge daraus).

Atomizität, Synchronisation und Volatilität sind unabhängige Attribute, werden jedoch normalerweise in Kombination verwendet, um eine ordnungsgemäße Thread-Zusammenarbeit für den Zugriff auf Variablen zu erzwingen.

Nachtrag (April 2016)

Der synchronisierte Zugriff auf eine Variable wird normalerweise mithilfe eines Monitors oder Semaphors implementiert . Hierbei handelt es sich um Mutex- Mechanismen auf niedriger Ebene (gegenseitiger Ausschluss), mit denen ein Thread ausschließlich die Kontrolle über eine Variable oder einen Codeblock erlangen kann. Alle anderen Threads müssen warten, wenn sie ebenfalls versuchen, denselben Mutex abzurufen. Sobald der besitzende Thread den Mutex freigibt, kann ein anderer Thread den Mutex nacheinander abrufen.

Nachtrag (Juli 2016)

Die Synchronisation erfolgt für ein Objekt . Dies bedeutet, dass durch das Aufrufen einer synchronisierten Methode einer Klasse das thisObjekt des Aufrufs gesperrt wird. Statisch synchronisierte Methoden sperren das ClassObjekt selbst.

Ebenso erfordert das Eingeben eines synchronisierten Blocks das Sperren des thisObjekts der Methode.

Dies bedeutet, dass eine synchronisierte Methode (oder ein synchronisierter Block) in mehreren Threads gleichzeitig ausgeführt werden kann, wenn sie unterschiedliche Objekte sperren. Es kann jedoch jeweils nur ein Thread eine synchronisierte Methode (oder einen synchronisierten Block) für ein bestimmtes einzelnes Objekt ausführen .

David R Tribble
quelle
25

flüchtig:

volatileist ein Schlüsselwort. volatileErzwingt, dass alle Threads den neuesten Wert der Variablen aus dem Hauptspeicher anstelle des Caches abrufen. Für den Zugriff auf flüchtige Variablen ist keine Sperrung erforderlich. Alle Threads können gleichzeitig auf den Wert der flüchtigen Variablen zugreifen.

Die Verwendung von volatileVariablen verringert das Risiko von Speicherkonsistenzfehlern, da bei jedem Schreiben in eine flüchtige Variable eine Beziehung hergestellt wird, die vor dem Lesen derselben Variablen auftritt.

Dies bedeutet, dass Änderungen an einer volatileVariablen für andere Threads immer sichtbar sind . Darüber hinaus bedeutet dies, dass ein Thread beim Lesen einer volatileVariablen nicht nur die letzte Änderung des Volatils sieht, sondern auch die Nebenwirkungen des Codes, der die Änderung ausgelöst hat .

Verwendungszweck: Ein Thread ändert die Daten und andere Threads müssen den neuesten Datenwert lesen. Andere Threads ergreifen einige Maßnahmen, aktualisieren jedoch keine Daten .

AtomicXXX:

AtomicXXXKlassen unterstützen die sperrenfreie threadsichere Programmierung einzelner Variablen. Diese AtomicXXXKlassen (wie AtomicInteger) beheben Speicherinkonsistenzfehler / Nebenwirkungen der Änderung flüchtiger Variablen, auf die in mehreren Threads zugegriffen wurde.

Verwendungszweck: Mehrere Threads können Daten lesen und ändern.

synchronisiert:

synchronizedist ein Schlüsselwort, das zum Schutz einer Methode oder eines Codeblocks verwendet wird. Das Synchronisieren der Methode hat zwei Auswirkungen:

  1. Erstens ist es nicht möglich, dass zwei Aufrufe von synchronizedMethoden für dasselbe Objekt verschachtelt werden. Wenn ein Thread eine synchronizedMethode für ein Objekt ausführt , synchronizedblockieren alle anderen Threads, die Methoden für denselben Objektblock aufrufen (Ausführung aussetzen), bis der erste Thread mit dem Objekt fertig ist.

  2. Zweitens wird beim synchronizedBeenden einer Methode automatisch eine Vorher-Beziehung zu einem nachfolgenden Aufruf einer synchronizedMethode für dasselbe Objekt hergestellt. Dies garantiert, dass Änderungen am Status des Objekts für alle Threads sichtbar sind.

Verwendungszweck: Mehrere Threads können Daten lesen und ändern. Ihre Geschäftslogik aktualisiert nicht nur die Daten, sondern führt auch atomare Operationen aus

AtomicXXXentspricht volatile + synchronized, obwohl die Implementierung unterschiedlich ist. AmtomicXXXerweitert volatileVariablen + compareAndSetMethoden, verwendet jedoch keine Synchronisation.

Verwandte SE-Fragen:

Unterschied zwischen flüchtig und synchronisiert in Java

Volatile Boolean vs AtomicBoolean

Gute Artikel zum Lesen: (Der obige Inhalt stammt aus diesen Dokumentationsseiten)

https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html

https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/package-summary.html

Ravindra Babu
quelle
2
Dies ist die erste Antwort, die tatsächlich die Vor-Vor-Semantik der beschriebenen Schlüsselwörter / Funktionen erwähnt, die wichtig sind, um zu verstehen, wie sie sich tatsächlich auf die Codeausführung auswirken. Antworten mit höheren Stimmen verfehlen diesen Aspekt.
Jhyot
5

Ich weiß, dass nicht zwei Threads gleichzeitig in den Synchronisierungsblock eingegeben werden können

Zwei Threads können nicht zweimal einen synchronisierten Block für dasselbe Objekt eingeben. Dies bedeutet, dass zwei Threads denselben Block für verschiedene Objekte eingeben können. Diese Verwirrung kann zu einem solchen Code führen.

private Integer i = 0;

synchronized(i) {
   i++;
}

Dies verhält sich nicht wie erwartet, da jedes Mal ein anderes Objekt gesperrt werden kann.

wenn dies wahr ist als Wie funktioniert dieses atomic.incrementAndGet () ohne Synchronisieren? und ist thread sicher?

Ja. Es wird keine Verriegelung verwendet, um die Gewindesicherheit zu erreichen.

Wenn Sie genauer wissen möchten, wie sie funktionieren, können Sie den Code für sie lesen.

Und was ist der Unterschied zwischen internem Lesen und Schreiben in Volatile Variable / Atomic Variable?

Die Atomklasse verwendet flüchtige Felder. Es gibt keinen Unterschied auf dem Gebiet. Der Unterschied besteht in den durchgeführten Operationen. Die Atomic-Klassen verwenden CompareAndSwap- oder CAS-Operationen.

Ich habe in einem Artikel gelesen, dass Thread eine lokale Kopie von Variablen hat. Was ist das?

Ich kann nur davon ausgehen, dass es sich um die Tatsache handelt, dass jede CPU ihre eigene zwischengespeicherte Speicheransicht hat, die sich von jeder anderen CPU unterscheiden kann. Um sicherzustellen, dass Ihre CPU eine konsistente Ansicht der Daten hat, müssen Sie Thread-Sicherheitstechniken verwenden.

Dies ist nur dann ein Problem, wenn der Speicher gemeinsam genutzt wird und mindestens ein Thread ihn aktualisiert.

Peter Lawrey
quelle
@Aniket Thakur bist du dir da sicher? Ganzzahl ist unveränderlich. Daher wird i ++ den int-Wert wahrscheinlich automatisch entpacken, inkrementieren und dann eine neue Ganzzahl erstellen, die nicht dieselbe Instanz wie zuvor ist. Wenn Sie versuchen, i final zu machen, werden beim Aufrufen von i ++ Compilerfehler angezeigt.
Fuemf5
2

Synchronized Vs Atomic Vs Volatile:

  • Volatile und Atomic gelten nur für Variablen, während Synchronized für die Methode gilt.
  • Flüchtig sorgen für Sichtbarkeit, nicht für Atomizität / Konsistenz des Objekts, während andere für Sichtbarkeit und Atomizität sorgen.
  • Flüchtige Variablen werden im RAM gespeichert und der Zugriff ist schneller, aber wir können die Thread-Sicherheit oder -Synchronisation ohne synchronisiertes Schlüsselwort nicht erreichen.
  • Synchronisiert implementiert als synchronisierter Block oder synchronisierte Methode, während beide nicht. Wir können sichere Codezeilen mit Hilfe eines synchronisierten Schlüsselworts einfädeln, während wir mit beiden nicht dasselbe erreichen können.
  • Synchronisiert kann dasselbe Klassenobjekt oder ein anderes Klassenobjekt sperren, während beide dies nicht können.

Bitte korrigieren Sie mich, wenn ich etwas verpasst habe.

Ajay Gupta
quelle
1

Eine flüchtige + Synchronisation ist eine narrensichere Lösung für eine Operation (Anweisung), die vollständig atomar ist und mehrere Anweisungen an die CPU enthält.

Sprich zum Beispiel: volatile int i = 2; i ++, was nichts anderes ist als i = i + 1; Dies macht i nach der Ausführung dieser Anweisung zum Wert 3 im Speicher. Dies beinhaltet das Lesen des vorhandenen Werts aus dem Speicher für i (das ist 2), das Laden in das CPU-Akkumulatorregister und das Berechnen des vorhandenen Werts um eins (2 + 1 = 3 im Akkumulator) und das Zurückschreiben dieses inkrementierten Werts zurück in die Erinnerung. Diese Operationen sind nicht atomar genug, obwohl der Wert von i flüchtig ist. Wenn ich flüchtig bin, ist nur garantiert, dass ein einzelnes Lesen / Schreiben aus dem Speicher atomar ist und nicht mit MEHRFACH. Daher müssen wir auch um i ++ synchronisiert haben, damit es eine narrensichere atomare Anweisung ist. Denken Sie daran, dass eine Anweisung mehrere Anweisungen enthält.

Hoffe die Erklärung ist klar genug.

Thomas Mathew
quelle
1

Der Java Volatile Modifier ist ein Beispiel für einen speziellen Mechanismus, der gewährleistet, dass die Kommunikation zwischen Threads erfolgt. Wenn ein Thread in eine flüchtige Variable schreibt und ein anderer Thread diesen Schreibvorgang sieht, informiert der erste Thread den zweiten über den gesamten Speicherinhalt, bis er den Schreibvorgang in diese flüchtige Variable ausgeführt hat.

Atomoperationen werden in einer einzigen Aufgabeneinheit ohne Störung durch andere Operationen ausgeführt. Atomare Operationen sind in Multithread-Umgebungen erforderlich, um Dateninkonsistenzen zu vermeiden.

Sai Prateek
quelle