为了缓解CPU和内存访问速度的矛盾,增加了速度更快的多级高速缓存。
CPU通过高速缓存进行数据读写有以下优势:
因为缓存脏数据写回主内存一般采用的是写回法,而非直写法,所以缓存和主存之间会有数据一致性问题。
原子性问题:不可中断的一系列动作,不会被线程调度机制打断,也不会被CPU响应中断打断。
可见性问题:CPU修改本地缓存的数据,采用写回法刷新缓存,在刷回之前,其他CPU无法看到最新的版本。
有序性问题:编译器重排序和CPU重排序调整了指令顺序,发挥指令并行能力,优化程序性能。如果排序后执行结果和程序顺序执行不一样,则存在有序性问题。
MESI协议采用缓存锁而非总线锁,锁粒度更小,CPU并行能力更强。
每个CPU会通过嗅探在总线上传播的数据来检查自己高速缓存中的值是否过期,当CPU发现自己缓存行对应内存地址被修改时,就会将当前CPU行设置为无效。当CPU对这个数据进行修改时就需要重新从主内存读取。
CPU1读取数据a,这一行数据状态为E独占状态,如果其他CPU读取数据a,那么这一行修改为S共享状态。如果CPU1修改了数据a,那么在CPU1中这一行修改为M状态,CPU2中这一行修改为I无效状态。如果CPU2再次读写数据a,需要CPU1将这一行刷回主存,CPU2再次从主存读取,确保可见性。
在代码编译阶段进行指令重排,不改变程序执行结果的情况下,为了提升效率,编译器对指令进行乱序编译。目的是与其等待阻塞指令完成,不如先去执行其他指令。与CPU重排序相比,编译器重排序能够完成更大范围、效果更好的乱序优化。
流水线和乱序执行是现代CPU基本都具有的特性。机器指令在流水线中经历取指、译码、执行、访存、写回等操作,每个阶段交给不同部件完成,可以由这些部件执行不同指令的不同阶段,提高并行能力。只要满足as-if-serial规则,处理顺序和程序顺序可以不一致。但是只能保证指令之间显式因果关系,无法保证隐式因果关系,即无法保证语义上不相关但程序逻辑上相关的操作序列按序执行。
as-if-serial规则可以保证单CPU执行时保证结果正确,不能保证多CPU情况下的执行结果正确。
内存屏障作用
volatile在x86处理器上被JVM编译后,汇编代码中会插入一条lock前缀指令,实现全屏障的作用。
JMM是一套规范,该规范定义了一个线程对共享变量写入时,如何确保对另一个线程可见,提供了合理的禁用缓存以及禁止重排序的方法核心价值是解决可见性和有序性。另外,JMM定义了一套抽象指令,由JVM编译为具体的机器指令,用于屏蔽不同硬件的差异性,保证Java程序在不同平台下对内存访问是一致的。
对于硬件内存来说只有寄存器、高速缓存、主存等概念,没有工作内存(线程私有数据区域,虚拟机栈)、主存(堆内存)之分。也就是说Java内存模型对内存的划分对硬件内存没有任何影响,因为JMM只是一种抽象,是一组规则,并不实际存在,对硬件来说都会存储到主存、寄存器或者高速缓存中。
JMM定义了一套主存和工作内存的交互协议,包含八种操作,并要求JVM具体实现必须保证每一种操作是原子的。
valatile的lock前缀指令能够触发CPU的MESI缓存一致性协议,锁住缓存行并通知其他CPU本地缓存中该行失效,确保不会有多个CPU同时修改共享变量。其他CPU访问共享变量时发现该缓存行失效,就会从主存重新加载,因此保证了可见性。
as-if-serial语义:无论如何重排序,都必须保证代码在单线程下运行正确。编译器和CPU不会对存在数据依赖的指令进行重排序。
happens-before语义:无论如何重排序,都必须保证在多线程下运行正确。为此,JMM会禁止特定类型的编译器重排序和指令重排序,提供跨线程的内存可见性。编译器会插入内存屏障指令,特定指令和两侧指令发生重排序,确保执行结果与程序顺序执行一致。
本质上,这些规则是解决各种场景在并发时的可见性问题:
valatile的lock前缀指令能够触发CPU的MESI缓存一致性协议,锁住缓存行并通知其他CPU本地缓存中该行失效,确保不会有多个CPU同时修改共享变量。其他CPU访问共享变量时发现该缓存行失效,就会从主存重新加载,因此保证了可见性。
为了实现volatile关键字语义的有序性,JVM编译器在生成字节码时会在指令序列插入内存屏障来禁止特定类型的处理器重排序。
1 可见性是通过CPU缓存一致性协议MESI来保证的。 2 原子性无法保证,CPU缓存一致性MESI的缓存锁可以保证单条volatile写指令是原子的,但是多线程修改共享变量时不止一条指令,比如i++就有三条指令,无法保证原子性。 3 有序性是通过内存屏障指令来保证的,可以在volatile操作与两侧指令发生重排。注:volatile读操作要求read、load、use要连续执行,不能插入其他指令,可以确保每次volatile读操作可以从主存读取到最新数据。 volatile写操作要求assign、store、write要连续执行,不能插入其他指令,可以确保每次volatile写操作可以立刻从工作内存写回主存。
volatile修饰的变量前面会有一条lock前缀指令,该指令有三个功能:
synchronize是互斥锁,由JVM实现,实际上是调用了操作系统的pthread_mutex_lock系统调用。每个Java对象都有一个监视器对象同生共死,获取锁失败的线程会进入监视器对象的阻塞队列等待被唤醒。
ACC_SYNCHRONIZE和monitorenter、monitorxit都是访问监视器对象的方式。
为什么synchronize可以保证原子性、可见性和有序性问题呢?
synchronize是互斥锁,可以保证原子性。 synchronize使用后unlock时会强制将修改的共享变量刷回主存,保证可见性。 synchronize修饰的临界区入口和出口这两个时间点是拥有和程序顺序执行一致的状态,并且禁止临界区和两侧指令发生重排序,但是不能禁止临界区内部指令重排。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。