Tiefes Verständnis des flüchtigen Schlüsselworts

Gedanken aus einem Sichtbarkeitsproblem

Schauen wir uns einen Code an:

public static boolean flg =false;
    public static void main(String[] args) throws InterruptedException {
    
    
        Thread thread=new Thread(()->{
    
    
            int i=0;
            while (!flg){
    
    
                i++;
                //1. System.out.println("i:="+i);

                // 2.Thread.sleep(1000);
                /*try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }*/
            }
        });
        thread.start();
        Thread.sleep(1000);
        flg=true;
    }

Operationsergebnis:
Fügen Sie hier eine Bildbeschreibung ein
Wenn Sie den ersten Kommentarcode oder den zweiten Kommentarcode loslassen, werden Sie feststellen, dass das Programm normal endet. Warum? Lassen Sie uns analysieren

Durch Drucken wird die Schleife beendet

  • Da die unterste Druckschicht das synchronisierte Schlüsselwort verwendet, zeigt dies an, dass println eine Sperroperation hat, und die Freigabe der Sperre synchronisiert die im Arbeitsspeicher enthaltenen Schreibvorgänge zwangsweise mit dem Hauptspeicher.

    public void println(String x) {
          
          
            synchronized (this) {
          
          
                print(x);
                newLine();
            }
        }
    
  • Unter dem Gesichtspunkt der E / A ist das Drucken im Wesentlichen eine E / A-Operation, und die E / A-Effizienz der Festplatte muss viel langsamer sein als die Berechnungseffizienz der CPU, sodass E / A der CPU Zeit zum Aktualisieren des Speichers geben kann, was zu diesem Phänomen führt. Wir können dies überprüfen, indem wir eine neue Datei definieren ()

Thread.sleep (lang)

  • Thread.sleep (long) führt zu einem Threadwechsel, der zu einer Ungültigmachung des Caches führt, und liest dann den neuesten Wert

flüchtig

Wir wissen, dass der Hauptgrund, warum das am Anfang des Artikels erwähnte Problem auftritt, die Sichtbarkeit ist. Um die Sichtbarkeit von Threads zu gewährleisten, schlug Java das Schlüsselwort volatile vor.

Was ist Sichtbarkeit?

In einer Multithread-Umgebung können Threads, die anschließend auf die Variable zugreifen, das aktualisierte Ergebnis möglicherweise nicht sofort oder sogar nie das aktualisierte Ergebnis lesen, nachdem ein Thread eine gemeinsam genutzte Variable aktualisiert hat. Dies ist eine weitere Manifestation von Thread-Sicherheitsproblemen: Sichtbarkeit.

Warum gibt es ein Sichtbarkeitsproblem?

1. Cache

Die Verarbeitungsleistung eines modernen Prozessors (CPU) ist viel besser als die Zugriffsrate des Hauptspeichers (DRAM). Die Zeit, die der Hauptspeicher benötigt, um eine Lese- und Schreiboperation auszuführen, reicht aus, damit der Prozessor Hunderte von Befehlen ausführen kann. Um die Lücke zwischen dem Prozessor und dem Hauptspeicher zu schließen, haben Hardwareentwickler einen Cache zwischen dem Hauptspeicher und dem Prozessor eingeführt , wie in der Abbildung gezeigt: Der
Fügen Sie hier eine Bildbeschreibung ein
Cache hat eine Zugriffsrate, die viel größer ist als die des Hauptspeichers. Speicherkomponenten mit viel geringerer Kapazität als der Hauptspeicher, und jeder Prozessor verfügt über einen eigenen Cache. Nach der Einführung des Caches befasst sich der Prozessor beim Ausführen von Lese- und Schreibvorgängen nicht direkt mit dem Hauptspeicher, sondern über den Cache.
Moderne Prozessoren verfügen im Allgemeinen über mehrere Cache-Ebenen, wie in der obigen Abbildung dargestellt. Es gibt Level 1-Cache (L1-Cache), Level 2-Cache (L2-Cache) und Level 3-Cache (L3-Cache). Ihre Zugriffsreihenfolge: L1> L2> L3.
Wenn mehrere Threads auf dieselbe gemeinsam genutzte Variable zugreifen, speichert der Prozessorcache jedes Threads eine Kopie der gemeinsam genutzten Variablen. Dies führt zu einem Problem: Wenn ein Prozessor die Kopierdaten aktualisiert , Woher wissen und reagieren andere Prozessoren angemessen? Dies beinhaltet Sichtbarkeitsprobleme. Wird auch als Cache-Kohärenzproblem bezeichnet

Cache-Konsistenzproblem (MESI)

Das MESI-Protokoll (Modified-Exclusive-Shared-Invalid) ist ein weit verbreitetes Cache-Kohärenzprotokoll. Das von x86-Prozessoren verwendete Kohärenzprotokoll basiert auf dem MESI-Protokoll.
Um die Datenkonsistenz sicherzustellen, unterteilt MESI den Cache-Eintragsstatus in vier Typen: Geändert, Exklusiv, Freigegeben, Ungültig und definiert eine Reihe von Nachrichten (Nachrichten), um die Lese- und Schreibvorgänge zwischen den verschiedenen Prozessoren zu koordinieren.

  • Ungültig (ungültig, als I markiert) bedeutet, dass die entsprechende Cache-Zeile keine gültige Kopie enthält, die der Speicheradresse entspricht. Dieser Status ist der Anfangsstatus des Cache-Eintrags
  • Geteilt (gemeinsam genutzt, als S bezeichnet) bedeutet, dass die entsprechende Cache-Zeile die Kopierdaten enthält, die der entsprechenden Speicheradresse entsprechen, und die Caches auf anderen Prozessoren auch die Kopierdaten enthalten, die derselben Speicheradresse entsprechen, wie in der Abbildung gezeigt:
    Fügen Sie hier eine Bildbeschreibung ein
  • Exklusiv (exklusiv, bezeichnet als E) bedeutet, dass die entsprechende Cache-Zeile die Kopierdaten enthält, die der entsprechenden Speicheradresse entsprechen, und der Cache auf allen anderen Prozessoren die Kopierdaten nicht beibehält, wie in der Abbildung gezeigt:
    Fügen Sie hier eine Bildbeschreibung ein
  • Modifiziert (modifiziert, markiert als M), was bedeutet, dass die entsprechende Cache-Zeile die aktualisierten Ergebnisdaten der entsprechenden Speicheradresse enthält. Im MESI-Protokoll kann jeweils nur ein Prozessor die Daten aktualisieren, die derselben Speicheradresse entsprechen. BILD: Das
    Fügen Sie hier eine Bildbeschreibung ein
    MESI-Protokoll definiert einen Satz von Nachrichten (Nachrichten) zum Lesen der Koordination der verschiedenen Prozessoren, Speicherschreibvorgang, als:
    Fügen Sie hier eine Bildbeschreibung ein
    Als nächstes werfen wir einen kurzen Blick auf das Flussdiagramm des Arbeitsprozess-MESI-Protokolls:
    Fügen Sie hier eine Bildbeschreibung ein
    Von oben Wir können eine Leistungsschwäche des MESI-Protokolls feststellen: Nachdem der Prozessor die Speicheroperation ausgeführt hat, muss er warten, bis alle anderen Prozessoren die entsprechenden Kopierdaten in seinem Cache zwischengespeichert haben, und die
    ungültige Bestätigungs- / Leseantwort von diesen Prozessoren erhalten. Erst nach der Nachricht können die Daten in den Cache geschrieben werden.
    Um die durch solche Wartezeiten verursachte Verzögerung von Schreibvorgängen zu vermeiden und zu verringern, haben Hardwareentwickler Schreibcache- und Ungültigkeitswarteschlangen eingeführt.
Schreibpuffer (Speicherpuffer) und ungültige Warteschlange (ungültige Warteschlange)

Der Schreibpuffer (Speicherpuffer, auch als Schreibpuffer bezeichnet) ist eine private Cache-Komponente im Prozessor mit einer Kapazität, die kleiner als der Cache ist. Nachdem der Schreibpuffer eingeführt wurde, behandelt der Prozessor die Operation wie folgt: Wenn der entsprechende Cache-Eintrag S ist, speichert der Prozessor zuerst die relevanten Daten der Schreiboperation (einschließlich Daten und Speicheradresse mit Operation) in dem Schreibpuffer Im Eintrag und beim asynchronen Senden der ungültigen Nachricht, dh der Ausführungsprozessor der Speicherschreiboperation, glaubt, dass die Schreiboperation abgeschlossen wurde, nachdem die relevanten Daten der Schreiboperation in den Schreibpuffer gestellt wurden, und wartet nicht darauf, dass andere Prozessoren ungültige Bestätigung / Lesen zurückgeben Die Antwortnachricht führt weiterhin andere Anweisungen aus, wodurch die Verzögerung des Schreibvorgangs verringert wird.
Warteschlange ungültig machen. Nach dem Empfang der Nachricht ungültig löscht der Prozessor nicht die Kopierdaten, die der in der Nachricht angegebenen Speicheradresse entsprechen, sondern sendet die Nachricht Nach dem Speichern in der Invalidierungswarteschlange wird die Meldung "Ungültige Bestätigung" zurückgegeben, wodurch die Wartezeit für den Ausführenden der Schreiboperation verringert wird.
Der Prozess ist wie folgt:
Fügen Sie hier eine Bildbeschreibung ein
Der Schreibpuffer und die Invalidierungswarteschlange bringen jedoch einige neue Probleme mit sich: die Neuordnung von Befehlen

2. Neuordnung der Anweisungen

Wir verwenden ein Beispiel, um das Problem der Neuordnung von Anweisungen ausführlich zu erläutern :

int data =0;
boolean ready=false;
void threadDemo1(){
    
    
    data=1; //S1
    ready=true; //S2

}
void threadDemo2(){
    
    
   while (!ready){
    
     //S3
       System.out.println(data);  //S4
   }
}

Angenommen, der Cache von CPU0 hat nur eine Kopie von ready und der Cache von CPU1 hat nur eine Kopie von Daten. Der
Ausführungsprozess ist wie folgt:
Fügen Sie hier eine Bildbeschreibung ein
Aus Sicht von CPU1 erzeugt dies ein Phänomen: S2 wird vor S1 ausgeführt

Speicherbarriere

Welche Art der Speicherumordnung vom Prozessor unterstützt wird, enthält Anweisungen, die die entsprechende Neuordnung verhindern können. Diese Anweisungen werden als Speicherbarrieren bezeichnet.
Speicherbarrieren können durch XY dargestellt werden, wobei X- und Y-Untertabellen Laden (Lesen) und Speichern darstellen. (schreiben). Die Funktion der Speicherbarriere besteht darin, eine Neuordnung zwischen einer X-Operation auf der linken Seite des Befehls und einer Y-Operation auf der rechten Seite des Befehls zu verhindern, um sicherzustellen, dass alle X-Operationen auf der linken Seite des Befehls vor der Y-Operation auf der rechten Seite des Befehls gesendet werden. Wie nachfolgend dargestellt:
Fügen Sie hier eine Bildbeschreibung ein

Prinzip

Das Prinzip der Flüchtigkeit wird tatsächlich unter Verwendung der zugrunde liegenden Speicherbarriere realisiert. Wir können den Pseudocode nach dem Hinzufügen des Schlüsselworts flüchtig betrachten:

	volatile int data =0;
    boolean ready=false;
    void threadDemo1(){
    
    
        data=1;
        //StoreStore 确保前后的写操作已经写入到高速缓存中
        ready=true;

    }
    void threadDemo2(){
    
    
        while (!ready){
    
    
            //LoadLoad 确保ready的读操作在data的读操作之前
            System.out.println(data);
        }
    }

Zusammenfassend: Flüchtige Regeln für die Speicherbarriere beim Lesen / Schreiben:

  • Fügen Sie nach jedem flüchtigen Lesevorgang die LoadLoad-Barriere und die LoadStore-Barriere ein
  • Fügen Sie vor und nach jedem flüchtigen Schreibvorgang eine StoreStore-Barriere und eine StoreLoad-Barriere ein

passiert-vor-Modell

Das Java Memory Model (JMM) definiert das Verhalten der flüchtigen, endgültigen und synchronisierten Schlüsselwörter und stellt sicher, dass ordnungsgemäß synchronisierte Java-Programme auf Prozessoren unterschiedlicher Architektur ausgeführt werden können.
In Bezug auf die Atomizität legt JMM fest, dass die Lese- und Schreiboperationen anderer Basisdatentypen als Long / Double- und Shared-Variablen von Referenztypen alle atomar sind. Darüber hinaus schreibt JMM ausdrücklich vor, dass Lese- und Schreiboperationen für flüchtig modifizierte Long / Double Shared-Variablen ebenfalls atomar sind.
Bei Sichtbarkeits- und Auftragsproblemen verwendet JMM das Vorher-Vorher-Modell, um die
Vorher-Vorherige-Regel wie folgt zu beantworten :

  • Programmablaufregeln : scheinbar serielle Semantik (als ob seriell). Das Ergebnis einer Aktion in einem Thread ist für andere Aktionen nach der Aktion in der Programmsequenz sichtbar, und diese Aktionen erscheinen dem Thread selbst als ausgeführt und vollständig in der Programmsequenz übergeben.
  • Monitorsperrregel : Die Freigabe der Monitorsperre erfolgt vor jeder nachfolgenden Anwendung für die Sperre.
    Hinweis: "Freigabe" und "Anwendung" müssen für denselben Typ von Sperrinstanz gelten, dh, die Freigabe einer Sperre hat keine Beziehung vor der Anwendung einer anderen Sperre
  • Regeln für flüchtige Variablen : Die Schreiboperation einer flüchtigen Variablen erfolgt vor jeder nachfolgenden Leseoperation für die Variable.
    Hinweis: Es muss sich um dieselbe flüchtige Variable handeln, und zweitens müssen die Lese- und Schreiboperationen für dieselbe flüchtige Variable eine zeitliche Abfolge haben.
  • Thread-Startregel : Rufen Sie die start () -Methode des Threads auf, bevor eine Aktion im gestarteten Thread ausgeführt wird.
  • Thread-Beendigungsregel : Jede Aktion in einem Thread wird ausgeführt, bevor eine Aktion von der Join-Methode des Threads ausgeführt wird, nachdem die Join-Methode zurückgegeben wurde
  • Transitivitätsregel : Wenn A vor B und B vor C auftritt, geschieht A vor C.

um zusammenzufassen

  • Volatile erreicht Sichtbarkeit durch Cache-Kohärenz
  • Volatile verbietet die Neuordnung von Anweisungen durch die Speicherbarriere und sorgt so für Ordnung
  • JMM verwendet das Vorgängermodell, um das Problem der Sichtbarkeit und Ordnung genauer zu beschreiben.

Ich denke du magst

Origin blog.csdn.net/xzw12138/article/details/106403512
Empfohlen
Rangfolge