volatile的底层实现
知识点
计算机组成
cpu读取数据
三级缓存
cpu读取数据的时候,先从寄存器中读取数据,如果没有然后从L1(一级缓存)中读取数据,如果没有然后从L2(二级缓存总读取数据),如果没有然后从L3中读取数据,L3中没有,从内存中读取数据,内存中还没有从磁盘中读取数据,拿到数据后在一级一级放回(L3到L2到L1到寄存器)
cpu线程切换
- 每个线程独占cpu寄存器中的数据,当第二个线程执行的之后,把寄存器中的数据保存起来,然后读取第二个线程的数据到寄存器,然后执行第二个线程
- 一核两线程cpu是,一核内有两个寄存器,在两个线程切换的时候,不需要切换寄存器中的数据
cpu缓存意义
按块预读取(也叫缓存行)(cache line)
缓存在读取的数据的时候并不是只拿需要的数据,这样缓存的意义也就没有了,获取一个数据的时候,其周边的数据也会顺便拿过来,这如果当这个数据执行完成后,马上会旁边的数据的情况下,可以立即从最近的缓存中直接拿到,而不用再次逐级读取
缓存行经过实践后一般缓存行大小为64BIt(字节)
缓存一致性:
两个核一个用到了x,一个用到了y,但是都把周边的数据读取到了缓存中,当核1执行完x的逻辑后,接着执行y的逻辑,这个时候两个核内的数据并没有同步,这个就是
解决方案
intel的cpu
因特尔通常使用MESI协议保证缓存一致性。也就是一个核对数据更改后,通知另一个cpu把这个数据状态改为Invalid
状态,然后该核执行的时候要到缓存中再次读取数据。
推荐文章:https://www.cnblogs.com/z00377750/p/9180644.html
然后项目在运行过程中,如果有大量的这样的缓存行,高并发情况下频繁写会造成效率的降低。
解决方案:缓存行对齐
让X和Y这个连个数据不在一个缓存行中,及在XY数据中间添加无用的数据,这样反而在高并发情况下频繁写会增加运行效率。
例如:
disruptor(世界上最快的单机MQ)中的RingBuffer;
jdk1.7中的集合
1
2
3
4class LongWithPadding {
long value;//8个字节
long p0, p1, p2, p3, p4, p5, p6;//7*8=56个字节,然后下一个value必然独占一个缓存行
}
然后在jdk1.8中添加了一个注解@sun.misc.Contended
,对某字段加上该注解则表示该字段会单独占用一个缓存行(Cache Line)
适用场景
主要适用于频繁写的共享数据上。如果不是频繁写的数据,那么 CPU 缓存行被锁的几率就不多,所以没必要使用了,否则不仅占空间还会浪费 CPU 访问操作数据的时间。
java线程内存模型
- java线程内存模型和CPU缓存模型类似,是基于CPU缓存模型来建立的,
java线程内存模型是标准化的,屏蔽掉了底层不同计算机的区别。
重排序
重排序是cpu内部的优化过程。
加入有三条指令,指令1,指令2,指令3;如果顺序执行,我必须等待指令1执行完毕,然后才能执行指令3,这样如果指令1在等待有个状态,就会造成cpu资源利用率下降。所以在执行指令1的通知,也可以执行执行2,指令3,只要保证最后执行的结果是争取的,那么这个就是可以的
在java中这个是非常常见的,例如有一个程序
1 | if(isStart) { |
这个并不是等待if(isStart)判断为ture后,才开始执行里面的for循环,在指令重排后,有可能是里面的for循环先执行,然后执行到一半后在进行判断,如果这个判断为false,则丢弃这个计算for循环的结果。这个指令重排是cpu内部做的,为的就是提高效率
内存屏障
一旦内存数据被推送到缓存,就会有消息协议来确保所有的缓存会对所有的共享数据同步并保持一致。这个使内存数据对CPU核可见的技术被称为内存屏障或内存栅栏。
大多数的内存屏障都是复杂的话题。在不同的CPU架构上内存屏障的实现非常不一样。
volatile底层实现
volatile的作用是
- 内存可见性
- 禁止指令重排序
可见性
volatile 是怎么实现共享变量在多个线程之间工作内存的可见性的呢?
JMM的8种数据原子操作:
read 读取,作用于主内存把变量从主内存中读取到本本地内存。
load 加载,把从主内存中读取的变量加载到本地内存的变量副本中
use 使用,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个
需要使用变量的值的字节码指令时将会执行这个操作。、
assign 赋值 它把一个从执行引擎接收到的值赋值给工作内存的变量,
每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store 存储 ,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
write 写入 ,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
lock 锁定 :作用于主内存的变量,把一个变量标识为一条线程独占状态。
unlock 解锁:作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
线程1:先把initFlag变量read读取出来,再load载入工作内存,use使用线程1执行代码!initFlag
线程2:先把initFlag变量read读取出来,再load载入工作内存,use使用线程2执行代码initFlag=true,
再assign重新赋值,store存储并写入主内存,write写入到主内存中的变量。(线程2对缓存行lock加锁,
write写入主内存后会解锁unlock,防止initFlag还未write写入主内存就被线程1读取为false)。
线程1:因为initFlag被volatile修饰,使用MESI缓存一致性协议,线程1cpu总线嗅探机制监听到了
initFlag值的修改,线程1中initFlag=false失效变为true退出循环继续执行,
体现了多线程同步运行共享变量副本的可见性。如果initFlag没有被volatile修饰,
线程1将感知不到initFlag的变化,一直循环下去停止不了。
jmm缓存不一致的问题解决办法
总线加锁(性能太低):
cpu从主内存读取数据到高速缓存,会在总线对这个数据加锁,这样其它cpu没法去读或写这个数据,直到这个cpu使用完数据释放锁之后其它cpu才能读取该数据,导致结果:改并行为串行。
MESI缓存一致性协议
多个cpu从主内存读取同一个数据到各自的高速缓存,当其中某个cpu修改了缓存里的数据,该数据会马上同步回主内存,其它cpu通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效。
禁止指令重排
为什么要禁止
为了提高程序执行的性能,编译器和执行器(处理器)通常会对指令做一些优化(重排序)
- 编译器重排序。编译器在不改变单线程语义的前提下,可以重新安排语句的执行顺序;
- 处理器重排序。如果不存在数据依赖性,处理器可以改变语句对机器指令的执行顺序
现在的操作系统执行命令一般都是流水线式的执行,为了保证cpu的执行效率。
volatile底层是通过lock前缀指令、内存屏障来实现的
参考:
多线程:https://www.bilibili.com/video/BV1aQ4y1P7Me?p=9&spm_id_from=pageDriver
可见性:https://blog.csdn.net/qq_22075913/article/details/106758864