Panda白话 - volatile

之前看的抓心挠肝,多看几遍混熟了一点~ 赶紧码下来~

volatile 是什么

volatile 是一个类型修饰符,只能修饰共享变量

volatile 什么用

在这里插入图片描述
看这个单词的意思,不稳定的,易变的,可以看出,专治这些易变易产生问题数据,嘎嘎

1、程序并发执行过程中,对共享变量的修改其他线程可见,即数据可见性、一致性
2、防止指令重排导致程序异常

先来做个知识储备和普及~

共享变量

共享变量指的是并发中,不同线程访问主存中同一数据的变量,主存是给所有CPU共享的,大家伙都要用这个变量,那它就是共享变量,
在这里插入图片描述
我们都知道每个线程工作时有自己的一份工作内存, 这个工作内存指的就是CPU寄存器和高速缓存(L1、L2、L3),高速缓存下面介绍,CPU组成结构如下图:
在这里插入图片描述

下图,可以看出,并发情况下,每个线程分配一个cpu,每个cpu里有一个高速缓存,缓存里存放cpu要执行的指令和数据,cpu只和本缓存打交道(CPU和内存之间是没有管脚的),缓存再和主存(内存)通过总线做数据同步,

在这里插入图片描述

高速缓存

当然上面组成结构也是一步步演变过来的,缓存的出现是为了解决CPU处理速度和主存IO速度差异过大问题,CPU执行完一个指令一直在等内存IO结束再去执行下一行指令,效率低,所以,CPU高速缓存的出现大大提高了CPU使用性能,
下表可以看到使用缓存速度的提升

名称IO速度
内存几十 - 几百时钟周期
磁盘几千万个时钟周期
L1 - 一级高速缓存1 - 2时钟周期
L2 - 二级高速缓存几十时钟周期
L2 - 三级高速缓存高端CPU才有

L1 -> L2 -> L3 级缓存,
技术难度 递减,
制造成本 递减,
存储容量 递增,

CPU 查找数据顺序 L1 -> L2 -> L3 级缓存,找到即 缓存命中
找不到即缓存未命中,未命中则根据缓存/内存映射 去主存中将整个缓存行数据同步到L1缓存
在这里插入图片描述
L1- 一级缓存 分为 :
d-cache 数据高速缓存 - 存CPU运行所需数据
i-cache 指令高速缓存 - 存CPU要执行的指令
在这里插入图片描述

RAM && ROM

既然说到内存,内存就是计算组成5大部件(运算器、存储器、控制器、IO设备)的存储器嘛,
存储器顾名思义,就是存数据的,分为RAM和ROM
RAM - Random-Access Memory 随机访问存储器,就是我们常说的内存条,存放运行时临时数据,与CPU直接交换数据,随时读写,访问速度快,关机就没了
RAM 分为
SRAM - 静态随机存储 - 用户高速缓冲存储器 - CPU中缓存,COMS+晶体管,无需刷新,功耗低,速度快
DRAM- 动态随机存储 - 用做计算机主存 - 内存条,电容存储+晶体管,需定时刷新,功耗大
ROM - Read-Only Memory 只读存储器 ,就是硬盘,可以持久化数据的

并发编程3大特性

上面说了那么多,总结下来就是,针对并发(多线程)场景,每个线程一个工作内存,线程间数据不共享,想改主存里共享数据的时候不能偷摸的,要告诉其他人,修改操作所有人可见

1、原子性
2、可见性 - 多线程共同访问共享变量时,有线程修改共享变量,其它线程需立即可获取到最新数据
3、有序性 - 程序按代码顺序执行,
为提升性能,编译器和处理器执行指令进行优化,对无数据依赖的指令行进行重排序,即指令重排
在这里插入图片描述

指令重排 - 并发问题

先来看一下经典的Double - Check 单例模式,问题来了,它是线程安全的吗,NO,

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleton;  
    }  
}  

上答案,问题出在singleton = new Singleton();,new对象是类的6大加载场景(自己查去)之一,类加载机制 加载 -> 连接(验证 -> 准备 -> 解析) -> 初始化
大概归为三个指令:
1.分配内存 memory = allocate()
2.初始化对象 instance(memory)
3. instace = memory 直接引用替换符号引用,栈中变量指向堆内存空间

当进行了指令重排,2,3顺序调换, 1->3 -> 2
那么就会出现这么一种情况:
当一个线程获取锁,判断实例singleton 为空,开始创建对象,执行指令1-分配内存,3-替换符号引用
Duang~ 此时另一个线程请求走到if (singleton == null)第一个判空,发现 诶,有引用,不为空,Duang~,直接返回return singleton;,那这个对象去使用,But~ 还没初始化呢,啥也没有,光有一块空地,这一使用肯定空指针异常了啊,so ~ 不安全

这个时候volatile就登场了,只要加上volatile稍微修饰一下,编译器和处理器就不会对指令进行重排,就不会有以上问题了。

volatile 支持可见性、有序性

volatile 只是修饰了一下共享变量,它是怎么做到有序性 - 防止指令重排的呢,以及线程间共享变量的可见性呢
诶,JVM 写volatile修饰的变量时,会生成一个Lock前缀的指令,这个指令就做了这个事

可见性

先来看可见性,volatile修饰后做了三件事:
1、volatile修饰的共享变量在CPU缓存中被修改会强制立即写入主存
2、volatile修饰的共享变量修改时会使其它线程工作内存(CPU中L1或L2缓存)中缓存行失效
3、由于其它线程缓存行失效,再次使用该变量时会去主存读取最新数据
以上三件事,达到数据修改可见性,就是谁一改,别人都知道

Lock指令怎么做到的呢?
Lock指令一看就是个上锁的动作啊,当一个线程要修改共享变量值时,会发送一个独占该变量的请求,其它线程有个“嗅探”技术,一直监控传输再总线上的数据,发现这个请求的数据,诶? 是共享变量,就会将本缓存中该数据所在缓存行置为失效,lock指令请求成功就会独占变量,别人都失效了,你随便改吧,然后锁缓存总线,改完写到本缓存(L1 、L2),然后立马从缓存回写到主存,然后释放锁,其它线程再用就去主存拉最新的

有个MESI - 缓存一致性原则,来保证每个CPU中缓存行做这事
在这里插入图片描述
状态转换:大概了解吧
在这里插入图片描述

有序性 - 禁止指令重排

有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障
屏障嘛,就是挡住了,屏障前后顺序不能颠倒,但是前后内部顺序还是可以重排的

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2
 
//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

例如上面代码,语句1 2 没有数据依赖,可能会重排,如果重排了,线程1先inited = true; //语句2
线程2 直接就走doSomethingwithconfig(context);,context还没初始化,就报空指针了,inited 用volatile修饰后,前后顺序就不能重排了

下面这个例子也挺好,就是flag被volatile修饰后,前后顺序不能重排,但是语句1和2,语句3和4还是可以自由重排的。

//x、y为非volatile变量
//flag为volatile变量
 
x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;         //语句4
y = -1;       //语句5

总结:
所以volatile 的神奇是JVM 约定好了,谁懂volatile修饰的变量需要先过我这关,执行Lock前缀指令,lock指令又回去约束CPU ,这个变量不是你自己的,修改要公之于众,其它处理器一直“嗅探”,哦? 有人动了,那我这个失效了,再用我就拿新的 - 可见性
在编译器和处理器执行指令优化时放一道屏障,拦住,前后顺序谁也过不去 - 有序性

但是不保证原子性,synchronized 抱枕原子性、可见性、有序性,但它是整个程序独占锁啊,性能低,volatile 轻量级锁,只锁了变量。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值