New 构建你的护城河   登录后该广告会消失
volatile | 涉及到JVM这一层,大概都会问
IT面试
596 ·
0 ·
2023-02-08 16:47:35
最新编辑原因:

现代计算机内存模型

  • cpu

  • 高速缓存

  • 一致性协议

  • 主内存

Java内存模型

enter image description here

  1. JMM是java虚拟机规范中所定义的一种内存模型,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别。

  2. Java内存模型(JavaMemoryModel)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量,存储到内存和从内存中读取变量这样的底层细节

JMM有以下规定

  1. 所有的共享变量都存储于主内存,这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题

  2. 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本

  3. 线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量

  4. 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成

所以存在可见性问题,如何解决?

  1. 加锁
    因为某一个线程进入synchronized代码块前后,线程会获得锁,清空工作内存,从主内存拷贝共享变量最新的值到工作内存成为副本,执行代码,将修改后的副本的值刷新回主内存中,线程释放锁。
    而获取不到锁的线程会阻塞等待,所以变量的值肯定一直都是最新的。

  2. Volatile修饰共享变量

    1. 每个线程操作数据的时候会把数据从主内存读取到自己的工作内存,如果他操作了数据并且修改了,那其他已经读取的线程的变量副本就会失效了,需要对数据进行操作又要再次去主内存中读取了。

    2. volatile保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值。

缓存一致性协议

eg. Intel的MESI

当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取

怎么发现数据是否失效呢?嗅探
每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己【缓存行对应的内存地址被修改】,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里

嗅探的缺点
总线风暴。由于Volatile的MESI缓存一致性协议,需要不断的从主内存嗅探和CAS不断循环,无效交互会导致总线带宽达到峰值。
所以不要大量使用Volatile,至于什么时候去使用Volatile什么时候使用锁,根据场景区分。

 

禁止指令重排序

什么是重排序?

为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。

重排序的类型有哪些呢?源码到最终执行会经过哪些重排序呢?

enter image description here
一般重排序可以分为如下三种:

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序

  2. 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;

  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。

as-if-serial

不管怎么重排序,单线程下的执行结果不能被改变。
编译器、runtime和处理器都必须遵守as-if-serial语义。

那Volatile是怎么保证不会被执行重排序的呢?

1. 内存屏障:

  1. 内存屏障是一组处理器指令(前面的8个操作指令),用于实现对内存操作的顺序限制

  2. java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序

2. happens-before

3. 无法保证原子性
就是一次操作,要么完全成功,要么完全失败。
假设现在有N个线程对同一个变量进行累加也是没办法保证结果是对的,因为读写这个过程并不是原子性的。
要解决也简单,要么用原子类,比如AtomicInteger,要么加锁(记得关注Atomic的底层)。

 

面试遇到的问题

volatile作用
  1. 变量可见性

  2. 防止指令重排序

  3. 保障变量单次读,写操作的原子性,但不能保证i++这种操作的原子性,因为本质是读,写两次操作

volatile如何保证线程间可见和避免指令重排

volatile可见性是有指令原子性保证的,在jmm中定义了8类原子性指令,比如write,store,read,load。而volatile就要求write-store,load-read成为一个原子性操作,这样子可以确保在读取的时候都是从主内存读入,写入的时候会同步到主内存中(准确来说也是内存屏障),指令重排则是由内存屏障来保证的,由两个内存屏障:

  1. 一个是编译器屏障:阻止编译器重排,保证编译程序时在优化屏障之前的指令不会在优化屏障之后执行。

  2. 第二个是cpu屏障:sfence保证写入,lfence保证读取,lock类似于锁的方式。java多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个lock指令,就是增加一个完全的内存屏障指令。


本作品系原创,采用《署名-非商业性使用-禁止演绎4.0 国际》许可协议.转载请说明出处
本文链接:https://www.upupor.com/u/7r9gbxR 复制

无内容

推荐阅读