关于Java内存模型的理解

文章讨论了多核CPU环境下,由于高速缓存、指令重排序和并发导致的线程安全问题。Java内存模型为了解决这些问题,提出了缓存一致性协议如MESI和内存屏障机制,以确保可见性、有序性和原子性。MESI协议用于维护缓存的一致性,而内存屏障则用于防止指令重排序带来的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

1. 背景

2. 线程安全问题

2.1 缓存不一致问题

2.2 CPU优化

2.3 内存屏障

3 Java内存模型


1. 背景

首先从为什么要有Java内存模型开始讲起。

  • 现在计算机往往都是多核的,每个核心下会有高速缓存。高速缓存的诞生是由于CPU和内存(主存)的速度存在差异,L1和L2缓存一般是每个核心独占一份的。
  • 为了让CPU提高运算效率,处理器可能会对输入的代码进行乱序执行,也就是指令重排序
  • 一次对数值的修改操作往往是非原子性的(比如i++实际上在计算机执行时会分成多个指令)。

总的来说,单线程下上面说的可见性、有序性、原子性都没问题,因为单线程意味着无并发,并且单线程下,编译器/runtime/处理器都必须遵守as-if-serial予以,意味着他们不会对数据依赖关系的操作做重排序。

CPU为了效率,有了高速缓存、指令重排序等,整个架构变得复杂,我们写程序的时候也需要充分利用CPU资源,所以不得不使用多线程。

2. 线程安全问题

多线程意味着并发,也就意味着需要考虑线程安全问题。

  • 缓存数据不一致:多个线程同时修改共享变量,CPU核心下的高速缓存是不同享的,那么cache与内存之间的数据同步怎么做?
  • CPU指令重排序在多线程下导致代码在非预期下执行,最终会导致结果存在错误的情况。

2.1 缓存不一致问题

1) 使用总线锁:某个核心在修改数据的过程中,其他核心均无法修改内存中的数据。(类似于独占内存的概念,只要有CPU在修改,那别的CPU就得等待当前CPU释放)

2) 缓存一致性协议(MESI):Modified(修改状态)、Exclusive(独占状态)、Share(共享状态)、Invalid(无效状态),根据对象状态做不同的策略,相较于总线锁,MESI的锁粒度更小了,所以性能会更高。

缓存一致性协议我认为可以理解为缓存锁,它针对的是缓存行进行加锁,所谓缓存行其实就是高速缓存存储的最小单位。

MESI协议的原理大概就是,当每个CPU读取共享变量之前,会先识别数据的对象状态(是修改、共享还是独占或者无效)。

  • 如果是独占,说明当前CPU将要得到的变量数据是最新的,没有被其他CPU所同时读取。
  • 如果是共享,说明当前CPU将要得到的变量数据还是最新的,有其他的CPU在同时读取,但没有被修改。
  • 如果是修改,说明CPU正在修改该变量的值,同时会向其他CPU发送该数据状态为Invalid的通知,得到其他CPU响应后(其他CPU将数据状态从共享变成Invalid),当前CPU将高速缓存的数据写到主存,并把自己的状态从从Modified变成Exclusive。
  • 如果是无效,说明当前数据是修改过的,需要从主存重新读取最新的数据。

2.2 CPU优化

MESI的关键在于某个CPU修改了数据时,需要同步通知其他CPU数据被修改了。

同步,意味着等待,所以CPU需要进行优化,也就是从同步变成异步。

在修改时同步告诉其他CPU,而现在则把最新修改的值写到Store Buffer中,并通知其他CPU要修改状态,随后CPU就直接返回干其他事情了,等接收到其他CPU的响应消息再将数据更新到高速缓存中。其他CPU接收到Invalid通知时,也会把接收到的消息放入Invalid queue就会直接返回告诉修改数据的CPU已经将状态修改为Invalid了。

1) 同核心读写共享变量问题

异步会带来新的问题,那我现在CPU修改完A值也写到Store Buffer了,CPU就可以干其他事情了,那如果CPU又接受指令需要修改A值但上一次修改的值还在Store Buffer中尚未修改至高速缓存呢?

所以CPU在读取时,需要去Store Buffer看看是否存在,存在则直接取,不存在才读主存的数据

2) 不同核心读写共享变量问题。

CPU1修改了A值,已经把修改后的值写到了Store Buffer并通知CPU2对该值进行Invalid操作,而CPU2可能还没收到Invalid通知就去做其他操作导致CPU2的还是旧值。

即便CPU2收到了Invalid通知,但CPU1的值还没写到主存所以CPU2再次想主存读取的时候也还是旧值。

总而言之,由于CPU对缓存一致性协议进行异步优化(Store Buffer、Invalid queue),很可能导致后面的指令查不到前面指令的执行结果(各个指令的执行顺序非代码执行顺序)。

2.3 内存屏障

为了解决乱序问题(可见性问题,也就是修改完没有及时同步到其他CPU),引出了内存屏障的概念。内存屏障其实就是把异步优化给禁用,分为三中类型:写屏障、读屏障、全能屏障

简单来说,屏障可以理解为在操作数据的时候往数据插入一条特殊指令,只要遇到这条指令,那前面的操作都得完成。

1) 写屏障

当CPU发现写屏障时,会把该指令之前存在于Store Buffer所有写指令刷入高速缓存,通过这种方式可以让CPU修改的数据可以立刻暴露给其他CPU,达到可见性的目的。

2) 读屏障

当CPU发现读屏障时,会把该指令之前存在于Invalid queue的所有指令都处理掉,通过这种方式确保当前CPU的缓存状态是正确的,于是读操作一定是读取最新的数据。

3) 全能屏障

包含了读屏障和写屏障,相当于读写屏障。

3 Java内存模型

由于不同CPU架构的缓存体系、缓存一致性协议、重排序策略不一样,提供的内存屏障指令也有差异,所以为了简化Java开发工作,Java封装了一套规范,也就是Java内存模型。

Java内存模型希望屏蔽各种硬件和操作系统的访问差异,保证了Java程序在各种平台下对内存的访问都能得到一致效果,目的是解决多线程存在的原子性、可见性和有序性问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值