Java内存模型(Java Memory Model)

本文详细解读了内存可见性、共享变量、主内存和工作内存的概念,介绍了Java内存模型JMM的作用,重点讲解了缓存一致性协议(如MESI)和总线嗅探机制,以及重排序和happens-before规则。此外,还深入剖析了内存屏障在确保可见性和顺序执行中的关键作用。

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

一. 基本概念

(1)什么是可见性:
内存可见性(Memory Visibility)是指当一条线程修改了某个变量的值,新值对于其他线程来说是可以立即得知的。

(2)共享变量:
一个变量在多个线程的工作内存中都存在副本。

(3)主内存:
保存了程序的所有变量。

(4)工作内存:
每个线程都有自己的独立工作内存,里面保存了该线程使用的变量的副本(主内存对该变量的一份拷贝)

二.Java内存模型(JMM)

Java 内存模型(简称 JMM)和内存区域是不一样的东西。内存区域是指 JVM 运行时将数据分区域存储,强调对内存空间的划分,即运行时数据区(Runtime Data Area)。

内存模型是定义了线程和主内存之间的抽象关系,即 JMM 定义了 JVM 在计算机内存(RAM)中的工作方式。

在这里插入图片描述
注:

  • 线程对共享变量的所有操作都必须从自己的工作内存中读写不能直接从主内存中读写。
  • 不同线程之间不能直接访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成。

三: 缓存一致性协议

为了解决这个问题,在早期的CPU当中,是通过在总线上直接加锁的形式来解决缓存不一致的问题。但是直接加锁太粗暴了,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。很明显这样做是不可取的。

所以就出现了缓存一致性协议。缓存一致性协议有MSI,MESI,MOSI,Synapse,Firefly及DragonProtocol等等。

3.1 MESI协议
最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。

  • Modify(修改):当缓存行中的数据被修改时,该缓存行置为M状态
  • Exclusive(独占):当只有一个缓存行使用某个数据时,置为E状态
  • Shared(共享):当其他CPU中也读取某数据到缓存行时,所有持有该数据的缓存行置为S状态
  • Invalid(无效):当某个缓存行数据修改时,其他持有该数据的缓存行置为I状态

它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

而这其中,监听和通知又基于总线嗅探机制来完成。

3.2 总线嗅探机制
嗅探机制其实就是一个监听器,回到我们刚才的流程,如果是加入MESI缓存一致性协议和总线嗅探机制之后:

  1. CPU1读取数据a=1,CPU1的缓存中都有数据a的副本,该缓存行置为(E)状态
  2. CPU2也执行读取操作,同样CPU2也有数据a=1的副本,此时总线嗅探到CPU1也有该数据,则CPU1、CPU2两个缓存行都置为(S)状态
  3. CPU1修改数据a=2,CPU1的缓存以及主内存a=2,同时CPU1的缓存行置为(S)状态,总线发出通知,CPU2的缓存行置为(I)状态
  4. CPU2再次读取a,虽然CPU2在缓存中命中数据a=1,但是发现状态为(I),因此直接丢弃该数据,去主内存获取最新数据

当我们使用volatile关键字修饰某个变量之后,就相当于告诉CPU:我这个变量需要使用MESI和总线嗅探机制处理。从而也就保证了可见性。

四、重排序和 happens-before 规则

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从 Java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:
在这里插入图片描述
JMM 属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

Java 编译器禁止处理器重排序是通过在生成指令序列的适当位置会插入内存屏障(重排序时不能把后面的指令重排序到内存屏障之前的位置)指令来实现的。

4.1 happens-before
从 JDK5 开始,Java 内存模型提出了 happens-before 的概念,通过这个概念来阐述操作之间的内存可见性。如果一个操作执行的结果需要对另一个操作可见(“可见”是指当一条线程修改了某个变量的值,新值对于其他线程来说是可以立即得知),那么这两个操作之间必须存在 happens-before 关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。
与程序员密切相关的happens-before规则如下:
1、程序顺序规则:一个线程中的每个操作,happens-before于该线程中任意的后续操作。
2、监视器锁规则:对一个锁的解锁操作,happens-before于随后对这个锁的加锁操作。
3、volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。
4、传递性规则:如果 A happens-before B,且 B happens-before C,那么A happens-before C。

注意:两个操作之间具有happens-before关系,并不意味前一个操作必须要在后一个操作之前执行!仅仅要求前一个操作的执行结果,对于后一个操作是可见的,且前一个操作按顺序排在后一个操作之前。

五:内存屏障(Memory Barrier)

内存屏障,又称内存栅栏,是一个CPU指令,基本上它是一条这样的指令:
1、保证特定操作的执行顺序。
2、影响某些数据(或则是某条指令的执行结果)的内存可见性。

编译器和CPU能够重排序指令,保证最终相同的结果,尝试优化性能。插入一条Memory Barrier会告诉编译器和CPU:不管什么指令都不能和这条Memory Barrier指令重排序。

Memory Barrier所做的另外一件事是强制刷出各种CPU cache,如一个 Write-Barrier(写入屏障)将刷出所有在 Barrier 之前写入 cache 的数据,因此,任何CPU上的线程都能读取到这些数据的最新版本。

java中volatile就是基于Memory Barrier实现的。如果一个变量是volatile修饰的,JMM会在写入这个字段之后插进一个Write-Barrier指令,并在读这个字段之前插入一个Read-Barrier指令。

这意味着,如果写入一个volatile变量a,可以保证:
1、一个线程写入变量a后,任何线程访问该变量都会拿到最新值。
2、在写入变量a之前的写入操作,其更新的数据对于其他线程也是可见的。因为Memory Barrier会刷出cache中的所有先前的写入。

六: 补充

线程执行互斥代码的过程?:

  • 1)获得互斥锁

  • 2)清空自己的工作内存

  • 3)从主内存中拷贝最新的副本到工作内存

  • 4)执行代码

  • 5)将更改后的变量的值刷新到主内存

  • 6)释放互斥锁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值