Maison >Java >javaDidacticiel >Explication détaillée du mot-clé Volatile de Java
Ce mot-clé Java volatile est utilisé pour marquer une variable Java comme "stockée dans la mémoire principale". Plus précisément, cela signifie que chaque lecture d'une variable volatile est lue depuis la mémoire principale de l'ordinateur, plutôt que depuis le cache du processeur, et que chaque écriture dans une variable volatile sera écrite dans la mémoire principale, plutôt que simplement dans le cache du processeur.
En fait, depuis Java5, le mot-clé volatile garantit non seulement que les variables sont écrites dans la mémoire principale, mais également lues depuis la mémoire principale. Je l'expliquerai dans la section suivante.
Garantie de visibilité du mot-clé volatile de Java
Ce mot-clé volatile de Java garantit la visibilité des changements variables à travers les fils de discussion. Celui-ci peut paraître un peu abstrait, alors développons-le.
Dans une application multithread, les threads opèrent sur des variables non volatiles. Chaque thread peut copier des variables de la mémoire principale dans le cache du CPU lorsqu'il travaille, pour des raisons de performances. Si votre ordinateur contient plusieurs processeurs, chaque thread peut s'exécuter sur un processeur différent. Cela signifie que chaque thread copiera les variables dans le cache CPU de différents processeurs. Comme le montre la figure ci-dessous :
En utilisant des variables non volatiles, il n'y a aucune garantie quand la JVM lira les données de la mémoire principale dans le cache du CPU, ou écrire des données du cache du processeur dans la mémoire principale. Cela peut entraîner plusieurs problèmes qui sont expliqués dans les sections suivantes.
Imaginez un scénario dans lequel deux threads ou plus accèdent à un objet partagé qui contient une variable de compteur déclarée, comme ceci :
public class SharedObject { public int counter = 0; }
Imaginez également que seul le thread 1 incrémente la variable compteur, mais que le thread 1 et le thread 2 lisent occasionnellement cette variable compteur.
Si la variable du compteur n'est pas déclarée volatile, il n'y a aucune garantie quand la variable du compteur sera réécrite dans la mémoire principale à partir du cache du CPU. Cela signifie que la variable du compteur dans le cache du processeur est différente de la valeur dans la mémoire principale. Comme le montre la figure ci-dessous :
Le problème selon lequel le thread ne peut pas voir la dernière valeur de cette variable est dû au fait qu'elle n'a pas été réécrite dans la mémoire principale. par d'autres discussions, n'appelez pas cela un problème de « visibilité ». Les mises à jour d'un fil sont invisibles pour les autres fils.
En déclarant la variable compteur comme volatile, toutes les écritures dans la variable compteur seront immédiatement réécrites dans la mémoire principale. Dans le même temps, toutes les lectures de la variable du compteur seront lues directement depuis la mémoire principale. Voici comment déclarer une variable compteur comme volatile :
public class SharedObject { public volatile int counter = 0; }
Déclarez une variable comme volatile, afin qu'elle soit garantie de cibler cette variable La visibilité des écritures sur d'autres threads.
Ce mot-clé volatile Java garantit l'ordre avant et arrière
Depuis Java5, ce mot-clé volatile garantit non seulement que les variables sont lues et écrites à partir de la mémoire principale. En fait, le mot-clé volatile garantit également cela :
Si le thread A écrit dans une variable volatile et que le thread B lit ensuite cette variable, alors avant en écrivant cette variable volatile, toutes les variables sont visibles par le thread A, et elles seront également visibles par le thread B après avoir lu cette variable volatile.
Les instructions de lecture et d'écriture des variables volatiles ne seront pas réorganisées par la JVM (tant que la JVM détecte que l'activité du programme issue de la réorganisation n'a pas changé, la JVM peut réorganiser les instructions pour des raisons de performances) ). Les instructions peuvent être réorganisées avant et après, mais les lectures ou écritures de mots clés volatiles ne sont pas mélangées à ces instructions. Quelles que soient les instructions qui suivent la lecture ou l'écriture d'une variable volatile, l'ordre de lecture ou d'écriture est garanti.
Ces expressions nécessitent une explication plus approfondie.
Lorsqu'un thread écrit une variable volatile, non seulement la variable volatile elle-même est réécrite dans la mémoire principale. Toutes les autres variables modifiées par ce thread avant d'écrire cette variable volatile seront également réécrites dans la mémoire principale. Lorsqu'un thread lit une variable volatile, il lit également toutes les autres variables qui sont réécrites dans la mémoire principale avec la variable volatile.
Regardez cet exemple :
Thread A: sharedObject.nonVolatile = 123; sharedObject.counter = sharedObject.counter + 1; Thread B: int counter = sharedObject.counter; int nonVolatile = sharedObject.nonVolatile;
Car avant d'écrire ce compteur volatile, le thread A écrit une variable non volatile non volatile, puis lorsque le thread Lorsque A écrit ce compteur (variable volatile), les variables non volatiles sont également réécrites dans la mémoire principale.
Parce que le thread B commence à lire le compteur de variable volatile, alors la variable compteur et la variable non volatile seront lues par le thread B de la mémoire principale vers le cache du processeur. À ce stade, le thread B verra également la variable non volatile écrite par le thread A.
Les développeurs peuvent utiliser cette garantie de visibilité étendue pour optimiser la visibilité des variables entre les threads. Au lieu de déclarer chaque variable volatile, seule une ou quelques variables doivent être déclarées volatiles. Voici un exemple :
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变量,这个就不能保证。
Considérations sur les performances du mot-clé volatile
La lecture et l'écriture de variables volatiles entraînent la lecture ou l'écriture de cette variable dans la mémoire principale. La lecture ou l’écriture dans la mémoire principale coûte plus cher que l’accès au cache du processeur. L'accès aux variables volatiles empêche également la réorganisation des instructions, qui est une technique standard d'augmentation des performances. Par conséquent, vous ne devez utiliser des variables volatiles que lorsque vous avez réellement besoin d’une forte visibilité de la variable.
Ce qui précède est l'explication détaillée du mot-clé Volatile de Java. Pour plus de contenu connexe, veuillez faire attention au site Web PHP chinois (www.php.cn) !