Java JMM详解

Java JMM内存模型详解

一、JMM 的设计

从 JMM 设计者的角度来说,在设计 JMM 时,需要考虑两个关键因素:

  • 程序员对内存模型的使用。程序员希望内存模型易于理解,易于编程。程序员希望基于一个强内存模型来编写代码。
  • 编译器和处理器对内存模型的实现。编译器和处理器希望内存模型对它们的束缚越少越好,这样它们就可以做尽可能多的优化来提高性能。编译器和处理器希望实现一个弱内存模型。

首先JMM不是“真实存在”,而是和多线程相关的一组“规范”,需要每个 JVM 的实现都要遵守这样的“规范”,有了 JMM 的规范保障,并发程序运行在不同的虚拟机得到出的程序结果才是安全可靠可信赖。

volatile、synchronized、final等,它们的实现原理都涉及 JMM。有了 JMM 的参与,才让各个同步工具和关键字能够发挥作用同步语义才能生效,使得我们开发出并发安全的程序。主要目的就是让 Java 程序员在各种平台下达到一致性访问效果。

二、重排序、原子性、可见性

参考JMM理论基础

  • 重排序:编译器、JVM、甚至 CPU 都有可能出于优化性能的目的,并不能保证各个语句执行的先后顺序与输入的代码顺序一致,而是调整了顺序。
    • 编译器优化:重排序需要需要保证重排序后,不改变单线程内的语义
    • CPU 重排序:和编译器类似,目的都是通过打乱顺序提高整体运行效率。
    • 内存“重排序”:不是真正意义的重排序,但是结果跟重排序有类似的成绩。参考硬件层面分析
  • 可见性:内存有主存和工作内存,每个线程只能够直接接触到工作内存,无法直接操作主内存,而工作内存中所保存的数据正是主内存的共享变量的副本
  • 原子性:基本数据类型变量、引用类型变量、声明为 volatile 的任何类型变量的访问读写是具备原子性的,但类似于 “基本变量++” / “volatile++” 这种复合操作并没有原子性。比如 i++;

三、Java 内存模型解决的问题

JMM 最重要的的三点内容:重排序、原子性、可见性,JMM 抽象出主存储器(Main Memory)和工作存储器(Working Memory)两种。

  • 主存储器是实例位置所在的区域,所有的实例都存在于主存储器内。
  • 工作存储器是线程所拥有的作业区,每个线程都有其专用的工作存储器。工作存储器存有主存储器中必要部分的拷贝,称之为工作拷贝,线程是无法直接对主内存进行操作。

从抽象角度看,JMM 定义了线程与主内存之间的抽象关系:

  • 线程之间的共享变量存储在主内存(Main Memory)中。
  • 每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。
  • 从更低的层次来说,主内存就是硬件的内存,而为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。
  • Java 内存模型中的线程的工作内存(working memory)是 cpu 的寄存器和高速缓存的抽象描述。

为了支持 JMM,Java 定义了 8 种原子操作(Action),用来控制主存与工作内存之间的交互:

  • read 读取:作用于主内存,将共享变量从主内存传动到线程的工作内存中,供后面的 load 动作使用。
  • load 载入:作用于工作内存,把 read 读取的值放到工作内存中的副本变量中。
  • store 存储:作用于工作内存,把工作内存中的变量传送到主内存中,为随后的 write 操作使用。
  • write 写入:作用于主内存,把 store 传送值写到主内存的变量中。
  • use 使用:作用于工作内存,把工作内存的值传递给执行引擎,当虚拟机遇到一个需要使用这个变量的指令,就会执行这个动作。
  • assign 赋值:作用于工作内存,把执行引擎获取到的值赋值给工作内存中的变量,当虚拟机栈遇到给变量赋值的指令,执行该操作。比如 int i = 1;
  • lock(锁定) 作用于主内存,把变量标记为线程独占状态。
  • unlock(解锁) 作用于主内存,它将释放独占状态。

总结:

  • 作用于主内存
    • 读取、写入、 锁定、解锁
  • 作用于工作内存
    • 载入、使用、 赋值、存储

四、内存屏障

JMM 通过 八个原子动作,内存屏障保证了并发语义关键字的代码能够实现对应的安全并发访问:

JMM 内存屏障可分为读屏障和写屏障,Java 的内存屏障实际上也是上述两种的组合,按需禁用缓存和编译优化。即Java 编译器在生成字节码时,会在执行指令序列的适当位置插入内存屏障来限制处理器的重排序。

  • Load-Load Barriers:load1 的加载优先于 load2 以及所有后续的加载指令,在指令前插入 Load Barrier,使得高速缓存中的数据失效,强制重新从主内存中加载数据。
  • Load-Store Barriers:确保 load1 数据的加载先于 store2 以及之后的存储指令刷新到内存。
  • Store-Store Barriers:确保 store1 数据对其他处理器可见,并且先于 store2 以及所有后续的存储指令。在 Store Barrie 指令后插入 Store Barrie 会把写入缓存的最新数据刷新到主内存,使得其他线程可见。
  • Store-Load Barriers:在 Load2 及后续所有读取操作执行前,保证 Store1 的写入对所有处理器可见。这条内存屏障指令是一个全能型的屏障,它同时具有其他 3 条屏障的效果,而且它的开销也是四种屏障中最大的一个

五、JMM 总结

JMM 是一个抽象概念,由于 CPU 多核多级缓存、为了优化代码会发生指令重排的原因,JMM 为了屏蔽细节,定义了一套规范,保证最终的并发安全。它抽象出了工作内存于主内存的概念,并且通过八个原子操作以及内存屏障保证了原子性、内存可见性、防止指令重排,使得 volatile 能保证内存可见性并防止指令重排、synchronised 保证了内存可见性、原子性、防止指令重排导致的线程安全问题,JMM 是并发编程的基础。

JMM 为程序中所有的操作定义了一个关系,称之为 「Happens-Before」原则,要保证执行操作 B 的线程看到操作 A 的结果,那么 A、B 之间必须满足「Happens-Before」关系,如果这两个操作缺乏这个关系,那么 JVM 可以任意重排序。

Happens-Before

  • 程序顺序原则:如果程序操作 A 在操作 B 之前,那么多线程中的操作依然是 A 在 B 之前执行。
  • 监视器锁原则:在监视器锁上的解锁操作必须在同一个监视器上的加锁操作之前执行。
  • volatile 变量原则:对 volatile 修饰的变量写入操作必须在该变量的读操作之前执行。
  • 线程启动原则:在线程对 Tread.start 调用必须在该线程执行任何操作之前执行。
  • 线程结束原则:线程的任何操作必须在其他线程检测到该线程结束前执行,或者从 Thread.join 中成功返回,或者在调用 Thread.isAlive 返回 false。
  • 中断原则:当一个线程在另一个线程上调用 interrupt 时,必须在被中断线程检测到 interrupt 调用之前执行
  • 传递性:如果操作 A 在操作 B 之前执行,并且操作 B 在操作 C 之前执行,那么操作 A 必须在操作 C 之前执行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值