关于jmm:小白也能看懂的Java内存模型

4次阅读

共计 5276 个字符,预计需要花费 14 分钟才能阅读完成。

前言

Java 并发编程系列开坑了,Java 并发编程能够说是中高级研发工程师的必备素养,也是中高级岗位面试必问的问题,本系列就是为了带读者们零碎的一步一步击破 Java 并发编程各个难点,突破屏障,在面试中所向无敌,拿到心仪的 offer,Java 并发编程系列文章仍然采纳图文并茂的格调,让小白也能秒懂。

Java 内存模型(Java Memory Model)简称 J M M,作为 Java 并发编程系列的开篇,它是 Java 并发编程的基础知识, 了解它能让你更好的明确线程平安到底是怎么一回事

内容纲要

硬件内存模型

程序是指令与数据的汇合,计算机执行程序时,是 C P U 在执行每条指令,因为 C P U 要从内存读指令,又要依据指令批示去内存读写数据做运算,所以执行指令就免不了与内存打交道,晚期内存读写速度与 C P U 处理速度差距不大,倒没什么问题。

C P U 缓存

随着 C P U 技术疾速倒退,C P U的速度越来越快,内存却没有太大的变动 ,导致内存的读写(IO)速度与C P U 的处理速度差距越来越大,为了解决这个问题,引入了缓存(Cache)的设计,在 C P U 与内存之间加上 缓存层 ,这里的缓存层就是指C P U 内的 寄存器与高速缓存L1,L2,L3

从上图中能够看出,寄存器最快,主内最慢,越快的存储空间越小,离 C P U 越近,相同存储空间越大速度越慢,离 C P U 越远。

C P U 如何与内存交互

C P U运行时,会将指令与数据从主存复制到缓存层,后续的读写与运算都是基于缓存层的指令与数据,运算完结后,再将后果从缓存层写回主存。

上图能够看出,C P U根本都是在和 缓存层 打交道,采纳 缓存设计 补救主存与 C P U 处理速度的差距,这种设计不仅仅体现在硬件层面,在日常开发中,那些并发量高的业务场景都能看到,然而凡事都有利弊,缓存尽管放慢了速度,同样也带来了在多线程场景存在的 缓存一致性问题 ,对于 缓存一致性问题 前面会说,这里大家留个印象。

Java 内存模型

Java 内存模型(Java Memory Model,J M M),后续都以 J M M 简称,J M M 是建设在 硬件内存模型根底上的形象模型 ,并不是物理上的内存划分,简略说,为了 使Java虚拟机 (Java Virtual Machine,J V M) 在各平台下达到统一的内存交互成果,须要屏蔽上游不同硬件模型的交互差别,对立标准,为上游提供对立的应用接口。

J M M是保障 J V M 在各平台下对计算机内存的交互都能保障成果统一的机制及标准

形象构造

J M M形象构造划分为线程本地缓存与主存,每个线程均有本人的本地缓存,本地缓存是线程 公有 的,主存则是计算机内存,它是 共享 的。

不难发现 J M M 与硬件内存模型差异不大,能够简略的把 线程 类比成 Core 外围 线程本地缓存 类比成 缓存层,如下图所示

尽管内存交互标准好了,然而多线程场景必然存在线程平安问题(竞争共享资源 ),为了使多线程能正确的同步执行,就须要保障并发的三大个性 可见性、原子性、有序性

可见性

当一个线程批改了共享变量的值,其余线程可能立刻得悉这个批改,这就是 可见性 ,如果无奈保障,就会呈现 缓存一致性的问题 J M M 规定,所有的变量都放在主存中,当线程应用变量时,先从缓存中获取,缓存未命中,再从主存复制到缓存,最终导致线程操作的都是本人缓存中的变量。

线程 A 执行流程

  • <span style=”color: Blue;”> 线程 A 从缓存获取变量a
  • <span style=”color: Blue;”> 缓存未命中,从主存复制到缓存,此时 a0
  • <span style=”color: Blue;”> 线程 A 获取变量a,执行计算
  • <span style=”color: Blue;”> 计算结果1,写入缓存
  • <span style=”color: Blue;”> 计算结果1,写入主存

线程 B 执行流程

  • <span style=”color: Blue;”> 线程 B 从缓存获取变量a
  • <span style=”color: Blue;”> 缓存未命中,从主存复制到缓存,此时 a1
  • <span style=”color: Blue;”> 线程 B 获取变量 a,执行计算
  • <span style=”color: Blue;”> 计算结果2,写入缓存
  • <span style=”color: Blue;”> 计算结果2,写入主存

AB两个线程执行完后,线程 A 与线程 B 缓存数据不统一,这就是 缓存一致性问题 ,一个是1,另一个是2,如果线程A 再进行一次 +1 操作,写入主存的还是 2,也就是说两个线程对a 共进行了 3+1,冀望的后果是3,最终失去的后果却是2

解决 缓存一致性问题 ,就要保障 可见性,思路也很简略,变量写入主存后,把其余线程缓存的该变量清空,这样其余线程缓存未命中,就会去主存加载。

线程 A 执行流程

  • <span style=”color: Blue;”> 线程 A 从缓存获取变量a
  • <span style=”color: Blue;”> 缓存未命中,从主存复制到缓存,此时 a0
  • <span style=”color: Blue;”> 线程 A 获取变量a,执行计算
  • <span style=”color: Blue;”> 计算结果1,写入缓存
  • <span style=”color: Blue;”> 计算结果 1,写入主存,并清空线程B 缓存 a 变量

线程 B 执行流程

  • <span style=”color: Blue;”> 线程 B 从缓存获取变量a
  • <span style=”color: Blue;”> 缓存未命中,从主存复制到缓存,此时 a1
  • <span style=”color: Blue;”> 线程 B 获取变量 a,执行计算
  • <span style=”color: Blue;”> 计算结果2,写入缓存
  • <span style=”color: Blue;”> 计算结果 2,写入主存,并清空线程A 缓存 a 变量

AB两个线程执行完后,线程 A 缓存是空的,此时线程 A 再进行一次 +1 操作,会从主存加载(先从缓存中获取,缓存未命中,再从主存复制到缓存)失去 2,最初写入主存的是3Java 中提供了 volatile 润饰变量保障 可见性 (本文重点是J M M,所以不会对volatile 做过多的解读)。

看似问题都解决了,然而下面形容的场景是建设在现实状况(线程有序的执行 ),理论中线程可能是并发( 交替执行 ),也可能是并行,只保障 可见性 依然会有问题,所以还须要保障 原子性

原子性

原子性 是指一个或者多个操作在 C P U 执行的过程中不被中断的个性,要么执行,要不执行,不能执行到一半,为了直观的理解什么是 原子性,看看上面这段代码

int a=0;
a++;
  • 原子性操作:int a=0只有一步操作,就是赋值
  • 非原子操作:a++有三步操作,读取值、计算、赋值

如果多线程场景进行 a++ 操作,仅保障 可见性 ,没有保障 原子性,同样会呈现问题。

并发场景(线程交替执行)

  • <span style=”color: Blue;”> 线程 A 读取变量 a 到缓存,a0
  • <span style=”color: Blue;”> 进行 +1 运算失去后果1
  • <span style=”color: Blue;”> 切换到 B 线程
  • <span style=”color: Blue;”>B线程执行残缺个流程,a=1写入主存
  • <span style=”color: Blue;”> 线程 A 复原执行,把后果 a=1 写入缓存与主存
  • <span style=”color: Blue;”> 最终后果谬误

并行场(线程同时执行)

  • <span style=”color: Blue;”> 线程 A 与线程 B 同时执行,可能线程 A 执行运算 +1 的时候,线程 B 就曾经全副执行实现,也可能两个线程同时计算完,同时写入,不论是那种,后果都是谬误的。

为了解决此问题,只有把多个操作变成一步操作,即保障 原子性

Java中提供了 synchronized 同时满足有序性、原子性、可见性 )能够保障后果的原子性( 留神这里的形容 ),synchronized 保障原子性的原理很简略,因为 synchronized 能够对代码片段上锁,避免多个线程并发执行同一段代码(本文重点是 J M M,所以不会对synchronized 做过多的解读)。

并发场景(线程 A 与线程 B 交替执行)

  • <span style=”color: Blue;”> 线程 A 获取锁胜利
  • <span style=”color: Blue;”> 线程 A 读取变量 a 到缓存,进行 +1 运算失去后果1
  • <span style=”color: Blue;”> 此时切换到了 B 线程
  • <span style=”color: Blue;”> 线程 B 获取锁失败,阻塞期待
  • <span style=”color: Blue;”> 切换回线程A
  • <span style=”color: Blue;”> 线程 A 执行完所有流程,主存a=1
  • <span style=”color: Blue;”> 线程 A 开释锁胜利,告诉线程 B 获取锁
  • <span style=”color: Blue;”> 线程 B 获取锁胜利,读取变量 a 到缓存,此时a=1
  • <span style=”color: Blue;”> 线程 B 执行完所有流程,主存a=2
  • <span style=”color: Blue;”> 线程 B 开释锁胜利

并行场景

  • <span style=”color: Blue;”> 线程 A 获取锁胜利
  • <span style=”color: Blue;”> 线程 B 获取锁失败,阻塞期待
  • <span style=”color: Blue;”> 线程 A 读取变量 a 到缓存,进行 +1 运算失去后果1
  • <span style=”color: Blue;”> 线程 A 执行完所有流程,主存a=1
  • <span style=”color: Blue;”> 线程 A 开释锁胜利,告诉线程 B 获取锁
  • <span style=”color: Blue;”> 线程 B 获取锁胜利,读取变量 a 到缓存,此时a=1
  • <span style=”color: Blue;”> 线程 B 执行完所有流程,主存a=2
  • <span style=”color: Blue;”> 线程 B 开释锁胜利

synchronized对共享资源代码段上锁,达到互斥成果,人造的解决了无奈保障 原子性、可见性、有序性 带来的问题。

尽管在并行场 A 线程还是被中断了,切换到了 B 线程,但它仍然须要期待 A 线程执行结束,能力持续,所以后果的原子性失去了保障。

有序性

在日常搬砖写代码时,可能大家都认为,程序运行时就是依照编写程序执行的,但实际上不是这样,编译器和处理器为了优化性能,会对代码做重排,所以语句理论执行的先后顺序与输出的代码程序可能统一,这就是 指令重排序

可能读者们会有疑难“指令重排为什么能优化性能?”,其实 C P U 会对重排后的指令做并行执行,达到优化性能的成果。

重排序前的指令

重排序后的指令

重排序后,对 a 操作的指令产生了扭转,节俭了一次 Load aStore a,达到性能优化成果,这就是重排序带来的益处。

重排遵循 as-if-serial 准则,编译器和处理器不会对 存在数据依赖关系 的操作做重排序,因为这种重排序会扭转执行后果(即不管怎么重排序,单线程程序的执行后果不能被扭转),上面这种状况,就属于数据依赖。

int i = 10
int j = 10
// 这就是数据依赖,int i 与 int j 不能排到 int c 上面去
int c = i + j

但也仅仅只是针对单线程,多线程场景可没这种保障,假如 A、B 两个线程,线程 A 代码段无数据依赖,线程 B 依赖线程 A 的后果,如下图(假如保障了可见性

禁止重排场景(i 默认 0)

  • <span style=”color: Blue;”> 线程 A 执行i = 10
  • <span style=”color: Blue;”> 线程 A 执行b = true
  • <span style=”color: Blue;”> 线程 B 执行 if(b) 通过验证
  • <span style=”color: Blue;”> 线程 B 执行i = i + 10
  • <span style=”color: Blue;”> 最终后果 i20

重排场景(i 默认 0)

  • <span style=”color: Blue;”> 线程 A 执行b = true
  • <span style=”color: Blue;”> 线程 B 执行 if(b) 通过验证
  • <span style=”color: Blue;”> 线程 B 执行i = i + 10
  • <span style=”color: Blue;”> 线程 A 执行i = 10
  • <span style=”color: Blue;”> 最终后果 i10

为解决重排序,应用 Java 提供的 volatile 润饰变量同时保障 可见性、有序性 ,被volatile 润饰的变量会加上 内存屏障禁止排序 (本文重点是J M M,所以不会对volatile 做过多的解读)。

三大个性的保障

个性 volatile synchronized Lock Atomic
可见性 能够保障 能够保障 能够保障 能够保障
原子性 无奈保障 能够保障 能够保障 能够保障
有序性 肯定水平保障 能够保障 能够保障 无奈保障

对于我

这里是阿星,一个酷爱技术的 Java 程序猿,公众号 「程序猿阿星」 里将会定期分享操作系统、计算机网络、Java、分布式、数据库等精品原创文章,2021,与您在 Be Better 的路上独特成长!。

非常感谢各位小哥哥小姐姐们能 看到这里,原创不易,文章有帮忙能够「点个赞」或「分享与评论」,都是反对(莫要白嫖)!

愿你我都能奔赴在各自想去的路上,咱们下篇文章见!

交个敌人

正文完
 0