Heim  >  Artikel  >  Java  >  Ausführliche Erklärung des Java-Schlüsselworts Volatile

Ausführliche Erklärung des Java-Schlüsselworts Volatile

黄舟
黄舟Original
2017-02-28 10:48:591939Durchsuche

Dieses flüchtige Java-Schlüsselwort wird verwendet, um eine Java-Variable als „im Hauptspeicher gespeichert“ zu markieren. Genauer gesagt bedeutet dies, dass jeder Lesevorgang einer flüchtigen Variablen aus dem Hauptspeicher des Computers und nicht aus dem CPU-Cache gelesen wird und jeder Schreibvorgang in eine flüchtige Variable in den Hauptspeicher geschrieben wird, anstatt nur in den CPU-Cache zu schreiben.

Tatsächlich sorgt das Schlüsselwort volatile seit Java5 nicht nur dafür, dass Variablen in den Hauptspeicher geschrieben, sondern auch aus dem Hauptspeicher gelesen werden. Ich werde es im nächsten Abschnitt erklären.

Sichtbarkeitsgarantie des flüchtigen Java-Schlüsselworts

Dieses flüchtige Java-Schlüsselwort garantiert die Sichtbarkeit variabler Änderungen über Threads hinweg. Das klingt vielleicht etwas abstrakt, also lassen Sie uns näher darauf eingehen.

In einer Multithread-Anwendung arbeiten Threads aus Leistungsgründen mit nichtflüchtigen Variablen. Wenn Ihr Computer mehr als eine CPU enthält, kann jeder Thread auf einer anderen CPU ausgeführt werden. Das bedeutet, dass jeder Thread die Variablen in den CPU-Cache verschiedener CPUs kopiert. Wie in der folgenden Abbildung dargestellt:


Bei Verwendung nichtflüchtiger Variablen gibt es keine Garantie dafür, dass die JVM Daten aus dem Hauptspeicher in den CPU-Cache liest. oder Daten aus dem CPU-Cache in den Hauptspeicher schreiben. Dies kann zu mehreren Problemen führen, die in den folgenden Abschnitten erläutert werden.

Stellen Sie sich ein Szenario vor, in dem zwei oder mehr Threads auf ein gemeinsames Objekt zugreifen, das eine deklarierte Zählervariable enthält, wie folgt:


public class SharedObject {

    public int counter = 0;

}


Stellen Sie sich außerdem vor, dass nur Thread 1 die Zählervariable erhöht, Thread 1 und Thread 2 jedoch gelegentlich diese Zählervariable lesen.

Wenn die Zählervariable nicht als flüchtig deklariert ist, gibt es keine Garantie, wann die Zählervariable aus dem CPU-Cache in den Hauptspeicher zurückgeschrieben wird. Das bedeutet, dass sich die Zählervariable im CPU-Cache vom Wert im Hauptspeicher unterscheidet. Wie in der folgenden Abbildung dargestellt:


Das Problem, dass der Thread den neuesten Wert dieser Variablen nicht sehen kann, liegt daran, dass er nicht in den Hauptspeicher zurückgeschrieben wurde von anderen Threads, nennen Sie es nicht ein „Sichtbarkeitsproblem“. Aktualisierungen von einem Thread sind für andere Threads unsichtbar.

Durch die Deklaration der Zählervariablen als flüchtig werden alle Schreibvorgänge auf die Zählervariable sofort in den Hauptspeicher zurückgeschrieben. Gleichzeitig werden alle Lesevorgänge der Zählervariablen direkt aus dem Hauptspeicher gelesen. So deklarieren Sie eine Zählervariable als flüchtig:


public class SharedObject {

    public volatile int counter = 0;

}


Deklarieren Sie eine Variable als flüchtig, damit sie garantiert als Ziel gilt Diese Variable Die Sichtbarkeit von Schreibvorgängen in andere Threads.

Dieses flüchtige Java-Schlüsselwort stellt die Reihenfolge von vorwärts und rückwärts sicher

Seit Java5 stellt dieses flüchtige Schlüsselwort nicht nur sicher, dass Variablen aus dem Hauptspeicher gelesen und geschrieben werden. Tatsächlich garantiert auch das Schlüsselwort volatile dies:


  • Wenn Thread A in eine flüchtige Variable schreibt und Thread B diese Variable anschließend liest, Dann vorher Beim Schreiben dieser flüchtigen Variablen sind alle Variablen für Thread A sichtbar, und sie sind auch für Thread B sichtbar, nachdem er diese flüchtige Variable gelesen hat.

  • Anweisungen zum Lesen und Schreiben flüchtiger Variablen werden von der JVM nicht neu angeordnet (solange die JVM erkennt, dass sich die Programmaktivität durch die Neuordnung nicht geändert hat, kann die JVM die Anweisungen neu ordnen aus Performancegründen) ). Anweisungen können vorher und nachher neu angeordnet werden, aber flüchtige Schlüsselwort-Lese- oder Schreibvorgänge werden nicht mit diesen Anweisungen vermischt. Unabhängig davon, welche Anweisungen einem Lese- oder Schreibvorgang in eine flüchtige Variable folgen, ist die Reihenfolge des Lesens oder Schreibens garantiert.

Diese Ausdrücke bedürfen einer tiefergehenden Erklärung.

Wenn ein Thread eine flüchtige Variable schreibt, wird nicht nur die flüchtige Variable selbst in den Hauptspeicher zurückgeschrieben. Alle anderen Variablen, die von diesem Thread vor dem Schreiben dieser flüchtigen Variablen geändert wurden, werden ebenfalls in den Hauptspeicher zurückgeschrieben. Wenn ein Thread eine flüchtige Variable liest, liest er auch alle anderen Variablen, die zusammen mit der flüchtigen Variablen in den Hauptspeicher zurückgeschrieben werden.

Sehen Sie sich dieses Beispiel an:

Thread A:
    sharedObject.nonVolatile = 123;
    sharedObject.counter     = sharedObject.counter + 1;

Thread B:
    int counter     = sharedObject.counter;
    int nonVolatile = sharedObject.nonVolatile;


Da Thread A vor dem Schreiben dieses flüchtigen Zählers eine nichtflüchtige nichtflüchtige Variable schreibt, und wenn die Thread Wenn A diesen Zähler (flüchtige Variable) schreibt, werden auch die nichtflüchtigen Variablen in den Hauptspeicher zurückgeschrieben.

Da Thread B beginnt, den Zähler für flüchtige Variablen zu lesen, werden die Zählervariable und die nichtflüchtige Variable von Thread B aus dem Hauptspeicher in den CPU-Cache gelesen. Zu diesem Zeitpunkt sieht Thread B auch die von Thread A geschriebene nichtflüchtige Variable.

Entwickler können diese erweiterte Sichtbarkeitsgarantie nutzen, um die Sichtbarkeit von Variablen zwischen Threads zu optimieren. Anstatt jede Variable als flüchtig zu deklarieren, müssen nur eine oder einige wenige Variablen als flüchtig deklariert werden. Hier ist ein Beispiel:

public class Exchanger {

    private Object   object       = null;
    private volatile hasNewObject = false;

    public void put(Object newObject) {
        while(hasNewObject) {
            //wait - do not overwrite existing new object
        }
        object = newObject;
        hasNewObject = true; //volatile write
    }

    public Object take(){
        while(!hasNewObject){ //volatile read
            //wait - don't take old object (or null)
        }
        Object obj = object;
        hasNewObject = false; //volatile write
        return obj;
    }
}


线程A可能会通过不断的调用put方法设置对象。线程B可能会通过不断的调用take方法获取这个对象。这个类可以工作的很好通过使用一个volatile变量(没有使用synchronized锁),只要只是线程A调用put方法,线程B调用take方法。

然而,JVM可能重排序Java指令去优化性能,如果JVM可以做这个没有改变这个重排序的指令。如果JVM改变了put方法和take方法内部的读和写的顺序将会发生什么呢?如果put方法真的像下面这样执行:

while(hasNewObject) {
    //wait - do not overwrite existing new object
}
hasNewObject = true; //volatile write
object = newObject;


注意这个volatile变量的写是在新的对象被真实赋值之前执行的。对于JVM这个可能看起来是完全正确的。这两个写的执行的值不会互相依赖。

然而,重排序这个执行的执行将会危害object变量的可见性。首先,线程B可能在线程A确定的写一个新的值给object变量之前看到hasNewObject这个值设为true了。第二,现在甚至不能保证对于object的新的值是否会写回到主内存中。

为了阻止上面所说的那种场景发生,这个volatile关键字提供了一个“发生前保证”。保证volatile变量的读和写指令执行前不会发生重排序。指令前和后是可以重排序的,但是这个volatile关键字的读和写指令是不能发生重排序的。

看这个例子:

sharedObject.nonVolatile1 = 123;
sharedObject.nonVolatile2 = 456;
sharedObject.nonVolatile3 = 789;

sharedObject.volatile     = true; //a volatile variable

int someValue1 = sharedObject.nonVolatile4;
int someValue2 = sharedObject.nonVolatile5;
int someValue3 = sharedObject.nonVolatile6;


JVM可能会重排序前面的三个指令,只要他们中的所有在volatile写执行发生前(他们必须在volatile写指令发生前执行)。

类似的,JVM可能重排序最后3个指令,只要volatile写指令在他们之前发生。最后三个指令在volatile写指令之前都不会被重排序。

那个基本上就是Java的volatile保证先行发生的含义了。

volatile关键字不总是足够的

甚至如果volatile关键字保证了volatile变量的所有读取都是从主内存中读取,以及所有的写也是直接写入到主内存中,但是这里仍然有些场景声明volatile是不够的。

在更早解释的场景中,只有线程1写这个共享的counter变量,声明这个counter变量为volatile是足够确保线程2总是看到最新写的值。

事实上,如果在写这个变量的新的值不依赖它之前的值得情况下,甚至多个线程写这个共享的volatile变量,仍然有正确的值存储在主内存中。换句话说,如果一个线程写一个值到这个共享的volatile变量值中首先不需要读取它的值去计算它的下一个值。

如果一个线程需要首先去读取这个volatile变量的值,并且建立在这个值的基础上去生成一个新的值,那么这个volatile变量对于保证正确的可见性就不够了。在读这个volatile变量和写新的值之间的短时间间隔,出现了一个竞态条件,在这里多个线程可能会读取到volatile变量的相同的值生成一个新的值,并且当写回到主内存中的时候,会互相覆盖彼此的值。

多个线程增加相同的值得这个场景,正好一个volatile变量不够的。下面的部分将会详细解析这个场景。

想象下,如果线程1读取值为0的共享变量counter进入到CPU缓存中,增加1并且没有把改变的值写回到主内存中。线程2读取相同的counter变量从主内存中进入到CPU缓存中,这个值仍然为0。线程2也是加1,并且也没有写入到主内存中。这个场景如下图所示:


线程1和线程2现在是不同步的。这个共享变量的真实值应该是2,但是每一个线程在他们的CPU缓存中都为1,并且在主内存中的值仍然是0.它是混乱的。甚至如果线程最后写他们的值进入主内存中,这个值是错误的。

什么时候volatile是足够的

正如我前面提到的,如果两个线程都在读和写一个共享的变量,然后使用volatile关键字是不够的。你需要使用一个synchronized在这种场景去保证这个变量的读和写是原子性的。读或者写一个volatile变量不会堵塞正在读或者写的线程。因为这个发生,你必须使用synchronized关键字在临界区域周围。

作为一个synchronized锁可选择的,你也可以使用在java.util.concurrent包中的许多原子数据类型中的一个。例如,这个AtomicLong或者AtomicReference或者是其他中的一个。

假如只有一个线程读和写这个volatile变量的值,其他的线程只是读取这个变量,然后读的这个线程就会保证看到最新的值了。不使用这个volatile变量,这个就不能保证。

Leistungsüberlegungen zum Schlüsselwort volatile

Das Lesen und Schreiben flüchtiger Variablen führt dazu, dass diese Variable in den Hauptspeicher gelesen oder geschrieben wird. Das Lesen oder Schreiben in den Hauptspeicher ist teurer als der Zugriff auf den CPU-Cache. Der Zugriff auf flüchtige Variablen verhindert auch die Neuordnung von Befehlen, eine Standardtechnik zur Leistungssteigerung. Daher sollten Sie flüchtige Variablen nur dann verwenden, wenn Sie wirklich eine starke Sichtbarkeit der Variablen benötigen.

Das Obige ist die detaillierte Erklärung des Java-Schlüsselworts Volatile. Weitere verwandte Inhalte finden Sie auf der chinesischen PHP-Website (www.php.cn)!


Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn