Compréhension approfondie de volatile (Java)

Prenez l'habitude d'écrire ensemble ! C'est le 11ème jour de ma participation au "Nuggets Daily New Plan · April Update Challenge", cliquez pour voir les détails de l'événement .

avant-propos

En plus du mot clé de synchronisation mentionné dans l'article précédent, qui peut réaliser la synchronisation, il existe un autre mot clé volatile en Java qui peut réaliser une synchronisation simple. La compréhension de la connaissance synchronisée peut être considérée comme une compréhension approfondie de la synchronisation (1) - première compréhension de la synchronisation .

définition

Volatile est un modificateur de fonctionnalité. Le rôle de volatile est un mot-clé d'instruction pour garantir que cette instruction ne sera pas omise en raison de l'optimisation du compilateur et nécessite une lecture directe de la valeur à chaque fois. Il a des applications dans de nombreuses langues et est un mot-clé en Java. Ses principales fonctions sont les suivantes :

  1. Garantit la visibilité de cette variable à tous les threads. Autrement dit, après qu'un thread a modifié volatile, les autres threads peuvent immédiatement percevoir la nouvelle valeur de cette variable.
  2. Désactiver les optimisations de réorganisation des instructions. Réorganisation des instructions : cela signifie que le CPU adopte une variable modifiée volatile qui permet d'envoyer séparément plusieurs instructions aux unités de circuit correspondantes dans l'ordre spécifié par le programme. Après l'affectation, un autre "load addl $0x0, (%esp ) est exécutée. " opération, cette opération équivaut à une barrière mémoire (lorsque les instructions sont réordonnées, les instructions suivantes ne peuvent pas être réordonnées à la position avant la barrière mémoire). Lorsqu'un seul CPU accède à la mémoire, une barrière mémoire n'est pas nécessaire.

Parlons de la façon dont volatile implémente les fonctions ci-dessus. Pour une meilleure compréhension, expliquons d'abord comment volatile rend la réorganisation des instructions interdite.

volatil et réorganisation des instructions

Nous comprenons d'abord le concept de réordonnancement des instructions.

réorganisation des instructions

大家有没有想过,我们编写的java代码,机器在执行的时候一定按我们编写的顺序一条一条执行吗?答案是否定的,因为我们编写的代码顺序很可能不是机器cpu执行较优的顺序,因此java内存模型允许编译器和处理器对在编译器或运行时对指令重排序以提高性能,并且只会对不存在的数据依懒性的指令进行重排序。

虽然指令排序能提高性能,但不代表所有的语句都会进行指令重排序,上面也说了指令间需要不存在数据依赖性,数据依赖性有以下3种类型:

类型 代码举例 说明
写后读 a=1;b=a 写一个变量后,在读这个变量
写后写 a=1;a=2 写一个变量后,又改变(写)这个变量的值
读后写 b=a;a=2 读一个变量后,又改变(写)这个变量的值
对于上面这三种类型,如果改变代码的执行顺序,很明显执行结果不符合预期。所以,编译器和处理器在重排顺序的时候,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。也就是说:在单线程环境下,指令执行的最终效果应当与器在顺序执行下的效果一致,否则这种优化便会失去意义。这句话有个专业术语叫做as-if-serial语义(有兴趣的同学可查略资料进一步了解)。

听起来指令重排序能提高性能,岂不妙哉,但与此同时也会带来一些问题,很容易想到在多线程情况下,会出现由于指令重排序导致一些非预期结果的出现。 而被volatile修饰的变量,在汇编层指令会对volatile变量操作的指令加一个lock前缀的汇编指令。若变量被修改后,会立刻将变量由工作内存回写到主存中。那么意味了之前的操作已经执行完毕。这就是内存屏障。

可见性

我们首先看看下面的例子来理解变量对所有线程的可见性,我们定义了一个类变量num初始值为0,使用addNum()对num加1,当我们开启N个线程(n>2)去执行addNum(),发现结果num的值并不一定等于N,当调大N时可以明显感知到结果很可能小于N。

/**
 * 测试指令重排序
 */
public class ReadThread {

    private static int num = 0;

    public static void addNum() {
        num++;
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10000; i++) {
            Thread addThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    addNum();
                }
            });
            addThread.start();
        }
        System.out.println(num);

    }

}
复制代码

运行结果不等于N的原因,是因为num++并不是一个原子操作,反编译后可以看到i++的执行指令如下:

image.png 各指令含义如下:

  • getstatic:获取指定类的静态域, 并将其压入栈顶
  • iconst_1:将int型1推送至栈顶
  • iadd:将栈顶两int型数值相加并将结果压入栈顶
  • putstatic:为指定类的静态域赋值
  • return:从当前方法返回void

通过上面指令解释大概可以猜测问题出现指令getstatic上,线程获取num值时,没有获取到num被其他线程修改后的最新值,导致最终结果不一致,这里面原因与java内存模型JMM有关,JMM中规定所有的变量都存储在主内存(Main Memory)中,每条线程都有自己的工作内存(Work Memory),线程的工作内存中保存了该线程所使用的变量的从主内存中拷贝的副本。线程对于变量的读、写都必须在工作内存中进行,而不能直接读、写主内存中的变量。同时,本线程的工作内存的变量也无法被其他线程直接访问,必须通过主内存完成。

image.png 对于普通共享变量,线程A将变量修改后,体现在此线程的工作内存。在尚未同步到主内存时,若线程B使用此变量,从主内存中获取到的是修改前的值,便发生了共享变量值的不一致,也就是出现了线程的可见性问题,这也是上述代码结果不符合预期的原因。

而当我们给变量num加上volatile后,便可以解决此问题,这是因为当对volatile变量执行写操作后,JMM会把工作内存中的最新变量值强制刷新到主内存,并且写操作会导致其他线程中的缓存无效。使得其他线程从主存中获取到volatile变量的最新值。

volatile为什么没有原子性

Volatile garantit la cohérence en lecture et en écriture. Mais lorsque le thread 2 a terminé l'instruction d'opération avec l'ancienne valeur et est sur le point de la réécrire dans la mémoire, l'atomicité ne peut pas être garantie.

Petit exemple : Lorsqu'on utilise git ou svn pour développer des projets en développement quotidien, il y a des troncs et des branches. Il y a une classe d'énumération utilisée par tout le projet. Lorsque Xiaohong modifie cette classe, il soumet immédiatement le tronc et informe les autres petits partenaires : " Vous utilisez cette classe Vous devez la tirer sur le coffre quand vous en avez besoin", mais pour le moment, Xiaoming a terminé le développement de l'ancienne version et soumet cette classe, ce qui entraîne un conflit.

performances de lecture et d'écriture volatiles

La consommation de performances en 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 mémoire dans le code natif pour s'assurer que le processeur ne s'exécute pas dans le désordre. exécution.

Épilogue

Cet article présente le rôle de volatile, y compris principalement la visibilité et l'interdiction de la réorganisation des instructions, et les raisons de sa visibilité. Quant à la manière de réaliser l'interdiction de la réorganisation des instructions, elle n'a pas été présentée en détail, impliquant du code Java, du bytecode, du code source Jdk, niveau assemblage, niveau matériel et autres implémentations sous-jacentes, si vous êtes intéressé, vous pouvez vérifier les informations vous-même.

おすすめ

転載: juejin.im/post/7085403237964070942