前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >面试系列之-Volatile原理(JAVA基础)

面试系列之-Volatile原理(JAVA基础)

作者头像
用户4283147
发布2023-08-21 20:19:27
2170
发布2023-08-21 20:19:27
举报
文章被收录于专栏:对线JAVA面试
代码语言:javascript
复制
package com.crazymakercircle.visiable;
public class VolatileVar{
    //使用volatile保障内存可见性
    volatile int var = 0;
    public void setVar(int var){
        System.out.println("setVar = " + var);
        this.var = var;
    }
    public static void main(String[] args){
        VolatileVar var = new VolatileVar();
        var.setVar(100);
    }
}

===> 汇编指令:
0x0000000003931be6: mov %r8d,0xc(%rdx)
0x0000000003931bea: lock addl $0x0,(%rsp);*putfield var;
- ..VolatileVar::setVar@27 (line 17)
0x000000000305016f: add $0x50,%rsp
0x0000000003050173: pop %rbp

由于共享变量var加了volatile关键字,因此在汇编指令中,操作var之前多出一个lock前缀指令lock addl,该lock前缀指令有三个功能:

  1. 将当前CPU缓存行的数据立即写回系统内存在对volatile修饰的共享变量进行写操作时,其汇编指令前用lock前缀修饰。lock前缀指令使得在执行指令期间,CPU可以独占共享内存(即主存)。对共享内存的独占,老的CPU(如Intel 486)通过总线锁方式实现。由于总线锁开销比较大,因此新版CPU(如IA-32、Intel64)通过缓存锁实现对共享内存的独占性访问,缓存锁(缓存一致性协议)会阻止两个CPU同时修改共享内存的数据;
  2. lock前缀指令会引起在其他CPU中缓存了该内存地址的数据无效:写回操作时要经过总线传播数据,而每个CPU通过嗅探在总线上传播的数据来检查自己缓存的值是否过期,当CPU发现自己缓存行对应的内存地址被修改时,就会将当前CPU的缓存行设置为无效状态,当CPU要对这个值进行修改的时候,会强制重新从系统内存中把数据读到CPU缓存;
  3. lock前缀指令禁止指令重排:lock前缀指令的最后一个作用是作为内存屏障(Memory Barrier)使用,可以禁止指令重排序,从而避免多线程环境下程序出现乱序执行的现象;
保证可见性

加了volatile关键字修饰的变量,只要有一个线程将主内存中的变量值做了修改,其他线程都将马上收到通知,立即获得最新值。当写线程写一个volatile变量时,JMM会把该线程对应的本地工作内存中的共享变量值刷新到主内存。当读线程读一个volatile变量时,JMM会把该线程对应的本地工作内存置为无效,线程将到主内存中重新读取共享变量。

volatile语义实现原理:

两个与CPU相关的专业术语:

  • 内存屏障(memory barriers):一组处理器指令,用于实现对内存操作的顺序限制;
  • 缓存行(cache line):CPU高速缓存中可以分配的最小存储单位。处理器填写缓存行时会加载整个缓存行;

volatile可见性的实现是借助了CPU的lock指令,lock指令在多核处理器下,可以将当前处理器的缓存行的数据写回到系统内存,同时使其他CPU里缓存了该内存地址的数据置为无效。通过在写volatile的机器指令前加上lock前缀,使写volatile具有以下两个原则:

  • 写volatile时处理器会将缓存写回到主内存;
  • 一个处理器的缓存写回到内存,会导致其他处理器的缓存失效;
volatile语义中的内存屏障

volatile关键字除了保障内存可见性外,还能确保执行的有序性。volatile语义中的有序性是通过内存屏障指令来确保的。为了实现volatile关键字语义的有序性,JVM编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,JMM建议JVM采取保守策略对重排序进行严格禁止,下面是基于保守策略的volatile操作的内存屏障插入策略。

  • 在每个volatile读操作的后面插入一个LoadLoad屏障;
  • 在每个volatile读操作的后面插入一个LoadStore屏障;
  • 在每个volatile写操作的前面插入一个StoreStore屏障;
  • 在每个volatile写操作的后面插入一个StoreLoad屏障;

volatile写操作的内存屏障插入策略为:在每个volatile写操作前插入StoreStore(SS)屏障,在写操作后面插入StoreLoad屏障;volatile读操作的内存屏障插入策略为:在每个volatile写操作后插入LoadLoad(LL)屏障和LoadStore屏障,禁止后面的普通读、普通写和前面的volatile读操作发生重排序;

volatile变量的复合操作不具备原子性的原理

对于关键字volatile修饰的内存可见变量而言,具有两个重要的语义:

(1)使用volatile修饰的变量在变量值发生改变时,会立刻同步到主存,并使其他线程的变量副本失效;

(2)禁止指令重排序:用volatile修饰的变量在硬件层面上会通过在指令前后加入内存屏障来实现,编译器级别是通过下面的规则实现的;

JMM对于volatile变量会有特殊的约束:

(1)使用volatile修饰的变量其read、load、use都是连续出现的,所以每次使用变量的时候都要从主存读取最新的变量值,替换私有内存的变量副本值(如果不同的话);

(2)其对同一变量的assign、store、write操作都是连续出现的,所以每次对变量的改变都会立马同步到主存中;

假设有两个线程A、B分别运行在Core1、Core2上,并假设此时的value为0,线程A、B也都读取了value值到自己的工作内存;

现在线程A将value变成1之后,完成了assign、store的操作,假设在执行write指令之前,线程A的CPU时间片用完,线程A被空闲,但是线程A的write操作没有到达主存。由于线程A的store指令触发了写的信号,线程B缓存过期,重新从主存读取到value值,但是线程A的写入没有最终完成,线程B读到的value值还是0。线程B执行完成所有的操作之后,将value变成1写入主存。线程A的时间片重新拿到,重新执行store操作,将过期了的1写入主存。线程A、B并发操作value时可能发生脏数据写入的流程,在高并发场景下,volatile变量一定需要使用Java的显式锁结合使用;

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-08-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 对线JAVA面试 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 保证可见性
  • volatile语义中的内存屏障
  • volatile变量的复合操作不具备原子性的原理
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档