并发编程系列-volatile详解
前言
面试过程中,常见的双层校验锁会引发出去使用volatile关键字这个问题点,相关面试官的考察点主要在于volatile对于共享变量的可见性上,小伙伴试着回答下面按这些问题,看下是否能正确回答上来呢
- volatile关键字的作用是什么
- volatile是如何实现可见性的(内存屏障)
- i++为啥不能保证原子性(分为读数据和写数据两个操作)
- volatile的应用场景
- Java内存模型是什么
简介
Volatile被称为轻量级的synchronize,运行时候开销比synchronize更小,我们知道volatile能够保证共享变量的可见性,禁止指令重排,今天一起研究下关于volatile是如何实现上述所讲的这些功能,首先我们先看一下Java内存模型相关知识。
内存模型
java内存模型,英文全称为Java Memory Model,简称JMM,JMM本身是不存在的,是为了我们方便理解抽象化的一种概念,是一组规则和规范,它定义了程序中各个变量的的访问方式,Java内存模型规定了所有内存变量都存在主存上,每个线程都是有自己的工作内存,线程对变量的操作都必须从主内存中,对变量的操作不能直接在主存中进行,并且线程不能访问其他线程的工作内存。(ps,处理器不直接在内存进行通信,而是先将内存中的数据读取到内存缓存中(硬件上称为cpu的L1,L2缓存)进行操作,这样干的原因是提高速度)
引入的问题
如果一个变量在每个CPU上都存在缓存(多线程情况下读取到自己的工作内存中),出现缓存不一致问题,我们来看下这个例子,如图所示
上面例子中i并发的时候可以通过以下两个方式去解决
- 通过在总线上加Lock锁的方式去解决
- 缓存一致性协议
volatile变量内存可见性是通过内存屏障实现的,内存屏障其实就是一组cpu指令。当我们对声明成一个volatile变量进行写操作,JVM底层会在对volatile共享变量的写操作加上lock前缀指令。我们来看一下这个lock指令。
lock指令
在早期奔腾,inter等处理器上,lock前缀会让处理器执行的时候产生一个lock#信号将总线进行锁定,其他CPU对内存读写请求会被阻塞,一直等到锁释放,后来的处理器逐渐使用缓存一致性协议(MESI)取代了这种方式,因为锁总线期间其他CPU无法访问内存,效率比较低。
缓存一致性协议
平常我们大多数计算器使用的缓存一致性协议都属于嗅探协议,所有内存的传输都是发生在一条共享总线上,所有的cpu都能看到这条总线,cpu除了不断嗅探总线上的数据进行数据交换,还会跟踪其他CPU缓存在做什么,只要CPU处理器将数据写入内存,其他CPU处理器感知之后就会将自己内存中缓存段设置为失效状态,使用的时候去主内存中取读。volatile变量就是通过这样的机制使得每个线程都能获取该变量的最新值。
内存屏障作用
内存屏障两个作用(如果我们变量使用了volatile修饰,就会在读数据的时候插入读屏障,可以让缓存中的数据失效,让从主内存中读。在写指令之前插入写屏障,让写入缓存的最新数据写入主内存)
- 先用这个内存屏障的指令必须先执行,后于这个内存屏障的指令必须后执行
- 内存可见
happen-before
happen-before关系是java内存模型中保证多线程操作可见性机制。简单来说就是决定变量对你是否可见的,是一个模糊的可见性定义,有如下表现形式
- 线程内执行的每一个操作都保证了happen-before后面的操作,这保证了程序书写的顺序规则
- 对于volatile变量,对于写操作,保证happen-before在随后的对该变量的读取操作(这样当我们对volatile变量写的时候,其他变量读取缓存值就会失效)
- 对象构建完成,保证了happen-before于fiazlize开始动作
来个例子
//x、y为非volatile变量,z为volatile变量
x = 2; //语句1
y = 0; //语句2
z = true; //语句3
x = 4; //语句4
y = -1; //语句5
由于flag变量是volatile变量,在进行指令重排序过程中,不会将语句3放到语句1和2的前面,同时也不会将语句3放到4和5的后面,执行3的时候,1,2必须执行完毕且1,2的结果对于3,4,5可见
volatile应用场景
- 单例模型双重校验(ps 详情请参看本博主单例模式系列总结文章)
巨人肩膀
zhuanlan.zhihu.com/p/137193948
闲谈
感觉有帮助的同学还请点赞关注,这将对我是很大的鼓励~,公众号有自己开始总结的一系列文章,需要的小伙伴还请关注下个人公众号程序员fly,希望能一起成长。