一、cpu多核并发缓存架构解析
JMM内存模型:java多线程内存模型跟cpu缓存模型类似,是基于cpu缓存模型来建立的,java线程内存模型是标准化的,屏蔽掉了底层不同计算机的区别
JMM数据原子操作
- read(读取):从主内存读取数据
- load(载入):将主内存读取到的数据写入工作内存
- use(使用):从工作内存读取数据来计算
- assign(赋值):将计算好的值重新赋值到工作内存中
- store(存储):将工作内存数据写入主内存
- write(写入);将store过去的变量的值赋值给主内存中的变量
- lock(锁定):将主内存变量加锁,标识为线程独占状态
- unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量
二、JMM缓存不一致问题
2.1 多核cpu缓存一致性协议(MESI)
多个cpu从主内存读取同一个数据到各自的高速缓存,当其中某个cpu修改了缓存里的数据,该数据会马上同步回主内存,其它cpu通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效MESI是指4个状态的首字母。每个cache line有四个状态,可用2个bit表示,它们分别是
缓存锁的核心机制是基于缓存一致性协议来实现的,一个处理器的缓存回写到内存会导致其他处理器的缓存无效,IA-32和Intel 64处理器使用MESI实现缓存一致性协议
2.2 Volatile可见性底层实现原理
底层实现主要是通过汇编lock前缀指令,它会锁定这块内存区域的缓存(缓存行锁定)并回写到主内存
IA-32和Intel 64架构软件开发者手册对lock指令的解释:
1. 会将当前处理器缓存行的数据立即写回到系统内存
2. 这个写回内存的操作会引起在其他cpu里缓存了该内存地址的数据无效(MESI协议)
3. 提供内存屏障功能,使lock前后指令不能重排序
2.3 java程序汇编代码查看
-server -Xcomp -XX:+UnlockDiagnosticVMOptions-XX:+PrintAssembly-XX:CompileCommand=compileonly,*VolatileVisibilityTest.prepareData
三、指令重排序与内存屏障
指令重排序三大特性:可见性,有序性,原子性
1、volatile保证可见性与有序性,但是不保证原子性,保证原子性需要借助synchronized这样的锁机制
2、指令重排序:在不影响单线程程序执行结果的前提下,计算机为了最大限度的发挥机器性能,会对机器指令重排序优化
3、重排序会遵循as-if-serial与happens-before原则
3.3.1 as-if-serial语义
as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度), (单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
3.3.2 happens-before 原则
只靠sychronized和volatile关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,幸运的是,从JDK 5开始, Java使用新的JSR-133内存模型,提供了happens-before原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据,happensbefore 原则内容如下
- 程序顺序原则:即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
- 锁规则:解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
- volatile规则: volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
- 线程启动规则:线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见
- 传递性: A先于B, B先于C那么A必然先于C
- 线程终止规则:线程的所有操作先于线程的终结, Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中
- 断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
- 对象终结规则:对象的构造函数执行,结束先于finalize()方法
3.4内存屏障:
JVM规范定义的内存屏障
JVM规定volatile需要实现的内存屏障
不同CPU硬件对于JVM的内存屏障规范实现指令不一样
Intel CPU硬件级内存屏障实现指令
- Ifence:是一种Load Barrier读屏障,实现LoadLoad屏障
- sfence:是一种Store Barrier写屏障,实现StoreStore屏障
- mfence:是一种全能型的屏障,具备Ifence和sfence的能力,具有所有屏障能力
JVM底层简化了内存屏障硬件指令的实现
- lock前缀:lock指令不是一种内存屏障,但是它能完成类似内存屏障的功能