多线程环境下变量可见性-volatile关键字

「这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战

多线程可见性问题分析

多个线程共用一个变量,有一个线程修改这个值,另一个线程读取这个值。如果不使用volatile,会导致读取线程一直读取到旧的值。这个问题本质是多线程的可见性问题。

主内存 及 CPU 缓存模型

计算机中CPU会从主内存中取数据指令执行计算操作,如果每次数据都从主内存中取,会因为CPU的速度远超过内存速度,频繁读取会降低性能,所以现代CPU会有自己的CPU缓存。

一个 CPU 有多个运行核心,我们把一个运行核心称为一个物理核,每个物理核都可以运行应用程序。每个物理核都拥有私有的一级缓存(Level 1 cache,简称 L1 cache),包括一级指令缓存和一级数据缓存,以及私有的二级缓存(Level 2 cache,简称 L2 cache)。

因为 L1 和 L2 缓存是每个物理核私有的,所以,当数据或指令保存在 L1、L2 缓存时,物理核访问它们的延迟不超过 10 纳秒,保证CPU计算效率非常高。

img

因为每个CPU有自己的本地缓存,每次计算的时候都是读自己的缓存。修改数据的线程也是被另一个CPU在自己的本地缓存中不断修改,两个都是在各自本地的高速缓存中读取和计算,所以多线程并发运行时就可能出现这个可见性问题。

所以多线程可见性问题最本质的问题其实是可以深究到CPU层面。

总线加锁机制和MESI缓存一致性协议

总线加锁机制是指 CPU处理数据时,通过锁定系统总线或者时内存总线,让其他CPU不具备访问内存的访问权限,进而来保证缓存的一致性。

总线加锁机制,效率太差,目前已经淘汰了。

目前比较流程的是 MESI 协议,缓存一致性协议。MESI通过修改后将数据强刷回主内存,以及嗅探探查主内存数据是否修改,若修改则将自己的缓存无效机制,来保证缓存一致性。

MESI是四个单词的首字母缩写,Modified修改,Exclusive独占,Shared共享,Invalid无效。

Java 内存模型

Java内存模型是基于CPU缓存模型来建立的。Java内存模型是标准化的,是为了屏蔽底层不同计算机的区别。

  • read 从主存读取
  • load 将主存读取到的值写入工作内存
  • use 从工作内存读取数据来计算
  • assign 将计算好的值重新赋值到工作内存中
  • store 将工作内存数据写入主存
  • write 将store过去的变量赋值给主存中的变量

img

因为工作内存不是每次都访问主内存,所以出现可见性问题。

volatile

volatile是什么?

在多线程并发编程可能产生三类问题,分别是可见性,原子性,有序性。

可见性:当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。

有序性:在并发时,程序的执行可能会出现乱序。编译器和指令器有时候为了提高代码运行效率高,会将指令重排序。

原子性:一个操作是不可中断的。即使是多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。例如多个线程对共享变量 int i = 0 进行并发 i++ 时。如果保证原子性则 i = 2 ,如果不是原子性 则 i 不一定是几,可能为 1。

volatile 可以保证可见性,有序性,但无法保证原子性。

volatile 如何使用

volatile 使用简单,只要在共享变量声明时,添加volatile关键字即可。

public class VolatileDemo {

    public volatile static int i = 0;
    
}
复制代码

voltaile 如何保证可见性

volatile 会保证修改后的工作内存中的数据立即刷会到主内存中,且会让其他工作内存中这个数据的缓存过期掉。进而保证可见性。

对于volatile修饰的变量,执行写操作,JVM会发送一条lock前缀指令给CPU,CPU计算完毕后立即将这个值写回主内存,同时因为有MESI缓存一致性协议,所以各个CPU都会对总线进行嗅探,自己本地的缓存是否被别人修改。

CPU发现其它线程修改了某个缓存的数据,CPU就会将本地缓存的数据过期,然后这个CPU上执行的线程读取那个变量的时候就会从主内存重新加载最新的数据。

volatile 如何保证有序性

编译器执行器可能对代码进行重排序,遵循happens-before原则。

happens-before原则其中有一个原则就是

volatile变量原则:对一个变量的写操作先行发生于后面对这个变量的读操作。

volatile的变量 会在读和写之前会插入内存屏障,禁止内存屏障前后代码的重排序。

volatile 为什么无法保证原子性

因为两个线程同时完成运算,同时执行 assign ,store 和 write 写回主内存,运算后的这个需要写回的结果跟工作内存中的变量值无关,所以无法保证原子性。

Guess you like

Origin juejin.im/post/7031429702246432798