Maison  >  Article  >  Java  >  Vous faire comprendre les variables volatiles - Programmation simultanée Java et initié de la technologie

Vous faire comprendre les variables volatiles - Programmation simultanée Java et initié de la technologie

php是最好的语言
php是最好的语言original
2018-07-26 15:29:521459parcourir

Le langage Java fournit un mécanisme de synchronisation légèrement plus faible, à savoir les variables volatiles, pour garantir que les autres threads sont informés des opérations de mise à jour des variables. Performances volatiles : la consommation de performances de lecture de volatile est presque la même que celle des variables ordinaires, mais l'opération d'écriture est légèrement plus lente car elle nécessite l'insertion de nombreuses instructions de barrière de mémoire dans le code natif pour garantir que le processeur ne s'exécute pas dans le désordre.

1. Variables volatiles

Le langage Java fournit un mécanisme de synchronisation légèrement plus faible, à savoir des variables volatiles , pour garantir que les autres threads sont informés des opérations de mise à jour des variables . Lorsqu'une variable est déclarée volatile, le compilateur et le runtime remarqueront que la variable est partagée et que, par conséquent, les opérations sur la variable ne seront pas réorganisées avec d'autres opérations de mémoire. Les variables volatiles ne sont pas mises en cache dans des registres ou dans un endroit invisible pour les autres processeurs, donc la lecture d'une variable de type volatile renvoie toujours la valeur écrite la plus récemment.

Aucune opération de verrouillage n'est effectuée lors de l'accès aux variables volatiles, donc le thread d'exécution ne sera pas bloqué. Par conséquent, les variables volatiles sont un mécanisme de synchronisation plus léger que le mot-clé synchronisé.

Vous faire comprendre les variables volatiles - Programmation simultanée Java et initié de la technologie

Lors de la lecture ou de l'écriture de variables non volatiles, chaque thread copie d'abord la variable de la mémoire vers le cache du processeur. Si l'ordinateur dispose de plusieurs processeurs, chaque thread peut être traité sur un processeur différent, ce qui signifie que chaque thread peut être copié dans un cache de processeur différent.

Et la variable déclarée est volatile. La JVM garantit que chaque fois que la variable est lue, elle est lue depuis la mémoire, en sautant l'étape de cache du CPU.

Lorsqu'une variable est définie comme volatile, elle aura deux caractéristiques :

1. Assurer la visibilité de cette variable sur tous les threads La "visibilité" ici est telle que mentionnée au début de. cet article. Comme mentionné ci-dessus, lorsqu'un thread modifie la valeur de cette variable, volatile garantit que la nouvelle valeur est immédiatement synchronisée avec la mémoire principale et actualisée depuis la mémoire principale immédiatement avant chaque utilisation. Mais les variables ordinaires ne peuvent pas faire cela. La valeur des variables ordinaires doit être transférée entre les threads via la mémoire principale (voir : Modèle de mémoire Java pour plus de détails).

2. Désactivez l'optimisation de la réorganisation des instructions. Pour les variables avec des modifications volatiles, une opération supplémentaire "load addl $0x0, (%esp)" est effectuée après l'affectation. Cette opération est équivalente à une barrière de mémoire (lorsque les instructions sont réorganisées, les instructions suivantes ne peuvent pas être réorganisées à la position avant la mémoire. barrière) ), lorsqu'un seul processeur accède à la mémoire, aucune barrière de mémoire n'est nécessaire (Qu'est-ce que la réorganisation des instructions : cela signifie que le processeur utilise une méthode qui permet d'envoyer plusieurs instructions séparément à chaque unité de circuit correspondante pour les traiter dans un ordre ; non spécifié par le programme).

performances volatiles :

La consommation de performances de lecture de volatile est presque la même que celle des variables ordinaires, mais l'opération d'écriture est légèrement plus lente car elle nécessite l'insertion de nombreuses instructions de barrière de mémoire dans le code natif pour s'assurer que le processeur n'exécute pas dans le désordre.

2. Visibilité de la mémoire

En raison du modèle de mémoire Java (JMM), toutes les variables sont stockées dans la mémoire principale et chaque thread possède sa propre mémoire de travail (cache).

Lorsque le thread fonctionne, il doit copier les données de la mémoire principale vers la mémoire de travail. De cette façon, toute opération sur les données est basée sur la mémoire de travail (l'efficacité est améliorée), et les données dans la mémoire principale et la mémoire de travail des autres threads ne peuvent pas être directement manipulées, puis les données mises à jour sont actualisées dans la mémoire principale. mémoire.

La mémoire principale mentionnée ici peut simplement être considérée comme de la mémoire tas, tandis que la mémoire de travail peut être considérée comme de la mémoire de pile.

Ainsi, lors d'une exécution simultanée, il peut arriver que les données lues par le thread B soient les données avant la mise à jour du thread A.

Évidemment, cela posera certainement des problèmes, donc le rôle de volatile apparaît :

Lorsqu'une variable est modifiée par volatile, l'opération d'écriture de n'importe quel thread sera immédiatement renvoyée vers la mémoire principale, et forcera l'effacement des données du thread qui a mis en cache la variable, et les dernières données doivent être relues à partir de la mémoire principale.

Après modification volatile, le thread n'obtient pas directement les données de la mémoire principale. Il doit quand même copier la variable dans la mémoire de travail.

Application de visibilité mémoire

Lorsque nous avons besoin de communiquer entre deux threads en fonction de la mémoire principale, la variable de communication doit être modifiée avec volatile :

public class Test {

private static /*volatile*/ boolean stop = false;

public static void main(String[] args) throws Exception {
    Thread t = new Thread(
            () -> {
                int i = 0;
                while (!stop) {
                    i++;
                 System.out.println("hello");
                }
            });
    t.start();

    Thread.sleep(1000);
    TimeUnit.SECONDS.sleep(1);
    System.out.println("Stop Thread");
    stop = true;
}
}

Si ce qui précède exemple n'est pas défini sur volatile, le fil peut ne jamais se terminer

Mais il y a un malentendu ici. Cette façon de l'utiliser peut facilement donner l'impression aux gens :

Opérations simultanées sur volatile. -les variables modifiées sont thread-safe.

Il est important de souligner ici que volatile ne garantit pas la sécurité des threads !

Le programme suivant :

public class VolatileInc implements Runnable {

private static volatile int count = 0; //使用 volatile 修饰基本数据内存不能保证原子性

//private static AtomicInteger count = new AtomicInteger() ;

@Override

public void run() {
    for (int i = 0; i < 100; i++) {
        count++;
        //count.incrementAndGet() ;

    }
}

public static void main(String[] args) throws InterruptedException {
    VolatileInc volatileInc = new VolatileInc();
    IntStream.range(0,100).forEach(i->{
        Thread t= new Thread(volatileInc, "t" + i);
        t.start();
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });

    System.out.println(count);
}
}

Lorsque nos trois threads (t1, t2, main) accumulent un int en même temps, nous constaterons que la valeur finale sera moins de 100 000.

这是因为虽然 volatile 保证了内存可见性,每个线程拿到的值都是最新值,但 count ++ 这个操作并不是原子的,这里面涉及到获取值、自增、赋值的操作并不能同时完成。

所以想到达到线程安全可以使这三个线程串行执行(其实就是单线程,没有发挥多线程的优势)。也可以使用 synchronize 或者是锁的方式来保证原子性。还可以用 Atomic 包中 AtomicInteger 来替换 int,它利用了 CAS 算法来保证了原子性。

三、指令重排序

内存可见性只是 volatile 的其中一个语义,它还可以防止 JVM 进行指令重排优化。

举一个伪代码:

int a=10 ;//1
int b=20 ;//2
int c= a+b ;//3

一段特别简单的代码,理想情况下它的执行顺序是:1>2>3。但有可能经过 JVM 优化之后的执行顺序变为了 2>1>3。

可以发现不管 JVM 怎么优化,前提都是保证单线程中最终结果不变的情况下进行的。

可能这里还看不出有什么问题,那看下一段伪代码:

 private static Map<String,String> value ;
 private static volatile boolean flag = fasle ;
  //以下方法发生在线程 A 中 初始化 Map
 public void initMap(){
 //耗时操作
 value = getMapValue() ;//1
 flag = true ;//2
}

 //发生在线程 B中 等到 Map 初始化成功进行其他操作
public void doSomeThing(){
while(!flag){
    sleep() ;
}
 //dosomething
 doSomeThing(value);
}

这里就能看出问题了,当 flag 没有被 volatile 修饰时,JVM 对 1 和 2 进行重排,导致 value都还没有被初始化就有可能被线程 B 使用了。

所以加上 volatile 之后可以防止这样的重排优化,保证业务的正确性。

指令重排的的应用

一个经典的使用场景就是双重懒加载的单例模式了:

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;
}

这里的 volatile 关键字主要是为了防止指令重排。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:

    1.给 instance 分配内存

    2.调用 Singleton 的构造函数来初始化成员变量

    3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)。

       但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

相关文章:

具体介绍java高并发中volatile的实现原理

Java中如何正确使用Volatile变量?

相关视频:

Java多线程与并发库高级应用视频教程

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn