面试被问,对java内存模型的理解?
一、cpu层级的缓存一致性问题,即临界资源的可见性问题
1、缓存一致性产生的原因及解决
从计算机的层面,cup和内存之间的数据处理效率有较大差异,cup在处理数据效率上远高于内存,大概差距在100倍,当cup的计算效率那么高,如果大部分时间都花在等待内存的读取上,则效率会下降很多,如何解决这个cup和内存之间处理性能差异导致的性能低下的问题?
2、cup引入高速缓存分层模型的概念,提升效率,如下图:
- 注意:由于L1,L2内存空间较小,势必会按照一定策略淘汰一些数据;
- 在L1,L2,L3的内部里面是怎么缓存数据的?引入缓存行的概念(CacheLine)
3、缓存行的概念:
比如在L1的内部是怎么存储数据的?
4、随着技术的发展,现在的cpu都是多核的,沿用上述的高速缓存模型,就会造成缓存不一致的问题:
5、cpu如何解决缓存不一致的问题?就是cpu层面的可见性问题(变量被修改了,在多线程环境下,对其他cpu持有的变量是否可见)
(1)MESI缓存一致性协议
- M:表示缓存行数据被修改了,并且没有同步到主内存。而且这个数据是当前CPU独占的,其他CPU内核的缓存没有这个数据。这个状态的数据是 安全的,不存在缓存一致性问题
- E:表示缓存行数据是独占的,并且这个数据没有被修改,和主内存的数据是一致的。这个状态的数据是 安全的,不存在缓存一致性问题
- S:表示缓存行数据是共享的,这个数据被多个CPU缓存在缓存行中。并且都与内存中的值是一致的。这个状态的数据是 安全的,不存在缓存一致性问题
- I:表示缓存行的数据是无效的,如果需要使用这个数据,需要重新去主内存拉取(那边同步完)。存在数据一致性问题
(2)在MESI缓存一致性协议下,变量的各个状态在数据变更时,状态是如何转换的?
- 一个处于M状态的缓存行,必须时刻监听所有试图读取当前缓存行对应的主内存数据地址的操作。如果坚挺到有其他内核要读取这个数据,必须在读取操作之前先将缓存行数据写回主内存。
- 一个处于S状态的缓存行,必须时刻监听该缓存行 无效 或者 独占 或者 修改 当前缓存行的请求,如果监听到,将当前缓存行状态设置为I。
- 一个处于E状态的缓存行,必须时刻监听视图 读取 当前缓存行对应的主内存地址的操作,如果监听到,将当前缓存行状态修改为S。
6、影响MESI缓存一致性协议的两个主要和常见的优化:
- StoreBuffer:在cup与L1之间有个StoreBuffer,离cup最近,空间更小,效率更高
(1)StoreBuffer,写缓冲器,cup执行write操作时,将数据写到StoreBuffer,不等待数据落到L1高速缓存,就立即返回执行下面的命令,以此来提升cup的处理效率;
(2)加上StoreBuffer这个优化,会影响MESI缓存一致性协议来保证缓存一致性;(因为StoreBuffer改了数据,并没有落到L1,没及时去触发数据的状态变更为M状态)
- Invalidate Queue:无效化队列,
(1)这个东西是处理Invalidate消息的,就是当一个消息从E状态或者S状态变更为M状态时,会通知其他持有变量的cpu将变量改为Invalidate,如果频繁且数据量多的情况下,一个一个去通知,影响效率;
(2)此时cpu加入一个Invalidate Queue队列,将需要通知的消息放到Invalidate Queue里面,相当于解耦和削峰
(3)这种操作也会对MESI缓存一致性协议产生影响,导致变量的状态变更不能及时触发,从而导致某些cpu读取到缓存里面的脏数据;
cpu加上这两个优化,能提升处理效率,同时也会带来缓存不一致的问题,如何解决这两个优化带来的缓存不一致问题?
加lock指令:将数据变更后立即写回主内存来保证缓存一致性
二、cpu和java都是乱序执行的,有序性问题
1、乱序问题的产生:
cup为了提升效率,还会有各种各样的优化,比如乱序执行,什么是乱序执行?
就是在读指令的同时可以执行不影响该读指令的其他指令;
在写指令的同时也可以进行合并写,多个指令一起写(合并写)(类似于批量写?);
2、如何解决乱序问题:
(1)在cpu硬件层面,通过加锁和加内存屏障保证不乱序执行
加内存屏障(Memory Fence)
(x86的cpu)cup指令:
Lfence:加载屏障
Sfence:写屏障
Mfence:读写屏障
无论哪种屏障,最终也是转换成cpu指令,实际也是加的lock指令前缀
实际上cpu还有一些别的指令 来保证不乱序执行
(2)在JVM层级是如何保证不乱序执行的?
JVM只是在软件层面进行了一些规范,具体怎么实现,由各个JVM厂商去实现,但不管怎么实现,都是依赖于cpu层级的实现:
JVM层级的实现,总结起来有4种,实际上就是sfence和lfence的组合,实际就是加了屏障的前后的指令不进行重排,具体看加的什么屏障
- store store fence
- load load fence
- store load fence
- load store fence
三、原子性
1、原子性
(1)首先,部分代码本身就是转换成一行指令,一行指令在cpu层本来就是原子性的:
比如int i = 1,基础类型的赋值操作,本身就是原子性的;
(2)但像i++ 或 new 一个对象,这种语句就不是原子性的,
比如i++, 首先得先从内存中load出来i的值,比如i=1,再在cpu内部执行++操作,i = 2,再将i的新值写到内存中;
比如new一个对象,首先得进行类加载,再申请内存,给变量赋默认值,再执行构造器方法,给变量赋初始值,再将对象的符号引用改为直接引用(地址引用);
2、cpu层面是如何保证原子性的?
cpu层面的原子性是通过lock指令实现的,类似于加锁,有两种:
总线锁:在cpu缓存和主内存之间的总线上加锁,效率相对低
缓存行锁:在缓存行上加锁
问题:有了缓存锁,还需要总线锁吗?需要,因为缓存行的大小有限制,针对大一点的数据必须使用总线锁
AtomicInteger:原子类,通过CAS(compare and swap)来实现整数的原子性,底层加cmpxch指令:
CAS: cmpxch指令,CAS(compare and swap)的执行流程:
四、
volatile:禁止指令重排
实现细节:能保证有序性,读写指令前后都加屏障保证禁止指令重排序,也能保证可见性,在变量进行变更后立即刷新到主内存,但不能保证原子性,原子性需要synchronized来保证
synchronized:
jvm层级,加monitor enter 和monitor exit ,由c/c++实现,调用底层的同步机制,在cpu指令层级,使用lock 指令实现;
实现细节:保证了原子性,可见性
happens-before原则:在jvm实现中,也是指在多线程情况下,哪些情况不能进行指令重排的问题
java的八大原子性操作:
待续~