Verwandte Konzepte des Speichermodells
Drei Konzepte in der gleichzeitigen Programmierung
Java-Speichermodell
Eingehende Analyse desVolatilSchlüsselworts
Szenarien für die Verwendung des flüchtigen Schlüsselworts
Diese Art von Variable wird normalerweise als gemeinsam genutzte Variable bezeichnet.
Das heißt, wenn eine Variable in mehreren CPUs zwischengespeichert wird Programmierung), dann kann es zu Cache-Inkonsistenzproblemen kommen
Um das Cache-Inkonsistenzproblem zu lösen, gibt es normalerweise zwei Lösungen:
Durch Hinzufügen von LOCK# zum Bus
Über das Cache-Kohärenzprotokoll
Diese beiden Methoden befinden sich auf Hardwareebene. Die Methode wird auf
Die obige Methode bereitgestellt 1 wird ein Problem haben, da andere CPUs während der Sperrzeit nicht auf den Speicher zugreifen können, was zu Ineffizienz führt.
Das bekannteste Cache-Kohärenzprotokoll ist das MESI-Protokoll von Intel Die in jedem Cache verwendete gemeinsam genutzte Variable ist konsistent: Wenn die CPU Daten schreibt und festgestellt wird, dass sie eine gemeinsam genutzte Variable ist, wird sie auch auf anderen CPUs gelöscht Wenn andere CPUs diese Variable lesen müssen, stellen sie fest, dass die Cache-Zeile, die die Variable in ihrem eigenen Cache speichert, ungültig ist Aus dem Speicher erneut lesen
Bei der gleichzeitigen Programmierung stoßen wir normalerweise auf die folgenden drei Probleme: Atomizitätsproblem, Sichtbarkeitsproblem, Bestellproblem
Atomarität: Ein Prozess, bei dem eine Operation oder mehrere Operationen entweder alle ausgeführt werden und nicht durch irgendwelche Faktoren unterbrochen werden
Sichtbarkeit bedeutet, dass ein Thread den geänderten Wert sofort sehen kann
3.3 Ordnung Ordnung: Das heißt, die Reihenfolge der Programmausführung erfolgt in der Reihenfolge des Codes. Aus der Codesequenz geht hervor, dass Anweisung 1 vor Anweisung 2 liegt. Wenn die JVM dies also tatsächlich ausführt Wird garantiert, dass Anweisung 1 vor Anweisung 2 ausgeführt wird? Warum kann es hier zu einer Duplizierung der Anweisung kommen? Im Folgenden wird erläutert, was eine Neuordnung von Anweisungen ist. Um die Effizienz des Programmbetriebs zu verbessern, kann der Prozessor nicht garantieren, dass die Ausführungsreihenfolge jeder Anweisung im Programm stimmt Die Reihenfolge ist dieselbe wie im Code, es wird jedoch sichergestellt, dass das endgültige Ausführungsergebnis des Programms mit dem Ergebnis der sequentiellen Ausführung des Codes übereinstimmt. Die Neuordnung von Anweisungen hat keinen Einfluss auf die Ausführung eines einzelnen Threads, sondern auf die Korrektheit der gleichzeitigen Ausführung von Threads. Mit anderen Worten: Damit gleichzeitige Programme korrekt ausgeführt werden können, müssen Atomizität, Sichtbarkeit und Ordnung gewährleistet sein. Solange einer davon nicht gewährleistet ist, kann es zu Fehlfunktionen des Programms kommen. 4. Java-Speichermodell In der Java Virtual Machine Specification wird versucht, ein Java-Speichermodell (Java MemoryBitte analysieren Sie, welche der folgenden Operationen atomare Operationen sind:
x = 10; //Anweisung 1
y = x; //Aussage 2
x++; //Aussage 3
x = x + 1; //Aussage 4
Tatsächlich ist nur Anweisung 1 eine atomare Operation, und die anderen drei Anweisungen sind keine atomaren Operationen.
Mit anderen Worten, nur einfaches Lesen und Zuweisen (und die Zahl muss einer Variablen zugewiesen werden, die gegenseitige Zuweisung zwischen Variablen ist keine atomare Operation) sind atomare Operationen.
Wie aus dem Obigen hervorgeht, garantiert das Java-Speichermodell nur, dass grundlegende Lese- und Zuweisungsoperationen atomare Operationen sind. Wenn Sie Atomizität für einen größeren Bereich von Operationen erreichen möchten, können Sie dies durch synchronisierte und Sperren erreichen .
Für die Sichtbarkeit stellt Java das Schlüsselwort volatile zur Verfügung, um die Sichtbarkeit sicherzustellen.
Wenn eine gemeinsam genutzte Variable flüchtig geändert wird, wird sichergestellt, dass der geänderte Wert sofort im Hauptspeicher aktualisiert wird. Wenn andere Threads ihn lesen müssen, wird der neue Wert aus dem Speicher gelesen.
Gewöhnliche gemeinsam genutzte Variablen können die Sichtbarkeit nicht garantieren, da nach der Änderung einer gewöhnlichen gemeinsam genutzten Variablen ungewiss ist, wann sie in den Hauptspeicher geschrieben wird. Wenn andere Threads sie lesen, ist der Speicher möglicherweise immer noch der ursprüngliche alte Wert Daher ist die Sichtbarkeit nicht garantiert.
Darüber hinaus kann die Sichtbarkeit auch durch synchronisiert und Lock gewährleistet werden. Synchronized und Lock können sicherstellen, dass nur ein Thread gleichzeitig die Sperre erhält und dann den Synchronisationscode ausführt, und die Variable wird geändert in den Hauptspeicher geleert, bevor die Sperre aufgehoben wird. Die Sicht ist somit gewährleistet.
Im Java-Speichermodell dürfen Compiler und Prozessor Anweisungen neu anordnen, der Neuordnungsprozess hat jedoch keinen Einfluss auf die Ausführung von Single-Thread-Programmen, sondern auf die Richtigkeit der gleichzeitigen Multithread-Ausführung.
In Java können Sie das Schlüsselwort volatile verwenden, um eine gewisse „Ordnung“ sicherzustellen (es kann die Neuordnung von Anweisungen verhindern). Darüber hinaus kann die Ordnung durch Synchronisierung und Sperre sichergestellt werden. Offensichtlich stellen Synchronisierung und Sperre sicher, dass zu jedem Zeitpunkt ein Thread den Synchronisierungscode ausführt.
Darüber hinaus verfügt das Java-Speichermodell über eine gewisse angeborene „Ordnung“, d. Wenn die Ausführungsreihenfolge zweier Operationen nicht aus dem Prinzip „Vorher geschieht“ abgeleitet werden kann, ist ihre Reihenfolge nicht garantiert und die virtuelle Maschine kann sie nach Belieben neu anordnen.
Das Folgende ist eine detaillierte Einführung in das Prinzip „Vorher passiert“:
Regeln für die Programmsequenz: Schreiben Sie innerhalb eines Threads entsprechend der Codereihenfolge vorne die Operation tritt vor dem nach
geschriebenen Vorgang auf. Sperrregel: Ein Entsperrvorgang erfolgt vor demselben Sperrvorgang nach derselben Sperre
flüchtig Variablenregeln: Eine Schreiboperation auf eine Variable erfolgt zuerst, bevor eine nachfolgende Leseoperation auf diese Variable erfolgt
Weitergaberegel: Wenn Operation A zuerst vor Operation B und Operation B ausgeführt wird zuerst vor Operation C, daraus kann geschlossen werden, dass Operation A zuerst vor Operation C ausgeführt wird
Thread-Startregel: Die start()-Methode des Thread-Objekts tritt hier zuerst auf Jede Aktion des Threads
Thread-Unterbrechungsregel: Der Aufruf der Thread-Interrupt()-Methode erfolgt zuerst, wenn der Code des unterbrochenen Threads das Auftreten des Interrupt-Ereignisses erkennt
Thread-Beendigungsregel: Alle Vorgänge in einem Thread werden zuerst ausgeführt, wenn der Thread beendet wird. Wir können anhand der Thread.join()-Methode und des Rückgabewerts von Thread.isAlive() erkennen, dass der Thread beendet wurde
Die vierte Regel spiegelt tatsächlich die transitive Natur des Prinzips „passiert vorher“ wider.
Einmal eine gemeinsam genutzte Variable (Mitgliedsvariable). einer Klasse, class Nachdem die statische Mitgliedsvariable (Variable) flüchtig geändert wurde, verfügt sie über zwei Semantikebenen:
stellt die Sichtbarkeit sicher, wenn verschiedene Threads mit dieser Variablen arbeiten, also ein Thread Ändert einen bestimmten Wert einer Variablen. Der neue Wert ist für andere Threads sofort sichtbar.
Das Nachbestellen von Anweisungen ist verboten.
Bezüglich der Sichtbarkeit schauen wir uns zunächst einen Codeabschnitt an. Wenn Thread 1 zuerst und Thread 2 später ausgeführt wird:
//线程1 boolean stop = false; while(!stop){ doSomething(); } //线程2 stop = true;
Dieser Code ist sehr typisch Viele Leute verwenden möglicherweise diese Markierungsmethode, wenn sie einen Thread unterbrechen. Aber wird dieser Code tatsächlich völlig korrekt ausgeführt? Das heißt, wird der Thread unterbrochen? Nicht unbedingt, vielleicht in den meisten Fällen, kann dieser Code den Thread unterbrechen, aber er kann auch dazu führen, dass der Thread nicht unterbrochen werden kann (obwohl diese Möglichkeit sehr gering ist, aber sobald dies geschieht, wird eine Endlosschleife verursacht).
Im Folgenden wird erklärt, warum dieser Code dazu führen kann, dass der Thread nicht unterbrochen werden kann. Wie bereits erläutert, verfügt jeder Thread während der Ausführung über seinen eigenen Arbeitsspeicher. Wenn also Thread 1 ausgeführt wird, kopiert er den Wert der Stoppvariablen und legt ihn in seinem eigenen Arbeitsspeicher ab.
Wenn dann Thread 2 den Wert der Stoppvariablen ändert, aber bevor er Zeit hat, ihn in den Hauptspeicher zu schreiben, schaltet Thread 2 um, um andere Dinge zu tun, dann kennt Thread 1 die Änderung des Stopps nicht Variable von Thread 2 , sodass der Zyklus fortgesetzt wird.
Aber nach der Verwendung der flüchtigen Änderung wird es anders:
Erstens: Die Verwendung des Schlüsselworts volatile erzwingt, dass der geänderte Wert sofort in den Hauptspeicher geschrieben wird >
Hier liegt ein Missverständnis vor. Das Schlüsselwort volatile kann Sichtbarkeit gewährleisten, aber der Fehler im obigen Programm besteht darin, dass es keine Atomizität gewährleistet. Die Sichtbarkeit kann nur sicherstellen, dass jedes Mal der neueste Wert gelesen wird, Volatilität kann jedoch nicht die Atomizität von Operationen an Variablen garantieren.
public class Test { public volatile int inc = 0; public void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保证前面的线程都执行完 Thread.yield(); System.out.println(test.inc); } }
根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。
把上面的代码改成以下任何一种都可以达到效果:
采用synchronized:
public class Test { public int inc = 0; public synchronized void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保证前面的线程都执行完 Thread.yield(); System.out.println(test.inc); } }
采用Lock:
public class Test { public int inc = 0; Lock lock = new ReentrantLock(); public void increase() { lock.lock(); try { inc++; } finally{ lock.unlock(); } } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保证前面的线程都执行完 Thread.yield(); System.out.println(test.inc); } }
采用AtomicInteger:
public class Test { public AtomicInteger inc = new AtomicInteger(); public void increase() { inc.getAndIncrement(); } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保证前面的线程都执行完 Thread.yield(); System.out.println(test.inc); } }
在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。atomic是利用CAS来实现原子性操作的(Compare And Swap),CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。
volatile能在一定程度上保证有序性。
volatile关键字禁止指令重排序有两层意思:
1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
举个例子:
//x、y为非volatile变量 //flag为volatile变量 x = 2; //语句1 y = 0; //语句2 flag = true; //语句3 x = 4; //语句4 y = -1; //语句5
由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。
并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。
这里探讨一下volatile到底如何保证可见性和禁止指令重排序的。
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
它会强制将对缓存的修改操作立即写入主存;
如果是写操作,它会导致其他CPU中对应的缓存行无效。
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
对变量的写操作不依赖于当前值(比如++操作,上面有例子)
该变量没有包含在具有其他变量的不变式中
实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
下面列举几个Java中使用volatile的几个场景。
状态标记量
volatile boolean flag = false; while(!flag){ doSomething(); } public void setFlag() { flag = true; }
volatile boolean inited = false; //线程1: context = loadContext(); inited = true; //线程2: while(!inited ){ sleep() } doSomethingwithconfig(context);
double check
class Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; } }
至于为何需要这么写请参考:
Das obige ist der detaillierte Inhalt vonAnalysieren Sie den Beispielcode der Java-Implementierung des flüchtigen Schlüsselworts vom Stammverzeichnis aus (Bild).. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!