前言
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;”> 缓存未命中,从主存复制到缓存,此时
a
是0
- <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;”> 缓存未命中,从主存复制到缓存,此时
a
是1
- <span style=”color: Blue;”> 线程
B
获取变量 a,执行计算 - <span style=”color: Blue;”> 计算结果
2
,写入缓存 - <span style=”color: Blue;”> 计算结果
2
,写入主存
A
、B
两个线程执行完后,线程 A
与线程 B
缓存数据不统一,这就是 缓存一致性问题 ,一个是1
,另一个是2
,如果线程A
再进行一次 +1
操作,写入主存的还是 2
,也就是说两个线程对a
共进行了 3
次+1
,冀望的后果是3
,最终失去的后果却是2
。
解决 缓存一致性问题 ,就要保障 可见性,思路也很简略,变量写入主存后,把其余线程缓存的该变量清空,这样其余线程缓存未命中,就会去主存加载。
线程 A 执行流程
- <span style=”color: Blue;”> 线程
A
从缓存获取变量a
- <span style=”color: Blue;”> 缓存未命中,从主存复制到缓存,此时
a
是0
- <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;”> 缓存未命中,从主存复制到缓存,此时
a
是1
- <span style=”color: Blue;”> 线程
B
获取变量 a,执行计算 - <span style=”color: Blue;”> 计算结果
2
,写入缓存 - <span style=”color: Blue;”> 计算结果
2
,写入主存,并清空线程A
缓存a
变量
A
、B
两个线程执行完后,线程 A
缓存是空的,此时线程 A 再进行一次 +1
操作,会从主存加载(先从缓存中获取,缓存未命中,再从主存复制到缓存)失去 2
,最初写入主存的是3
,Java
中提供了 volatile
润饰变量保障 可见性 (本文重点是J M M
,所以不会对volatile
做过多的解读)。
看似问题都解决了,然而下面形容的场景是建设在现实状况(线程有序的执行 ),理论中线程可能是并发( 交替执行 ),也可能是并行,只保障 可见性 依然会有问题,所以还须要保障 原子性。
原子性
原子性 是指一个或者多个操作在 C P U
执行的过程中不被中断的个性,要么执行,要不执行,不能执行到一半,为了直观的理解什么是 原子性,看看上面这段代码
int a=0;
a++;
- 原子性操作:
int a=0
只有一步操作,就是赋值 - 非原子操作:
a++
有三步操作,读取值、计算、赋值
如果多线程场景进行 a++
操作,仅保障 可见性 ,没有保障 原子性,同样会呈现问题。
并发场景(线程交替执行)
- <span style=”color: Blue;”> 线程
A
读取变量a
到缓存,a
是0
- <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 a
和Store 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;”> 最终后果
i
是20
重排场景(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;”> 最终后果
i
是10
为解决重排序,应用 Java 提供的 volatile
润饰变量同时保障 可见性、有序性 ,被volatile
润饰的变量会加上 内存屏障禁止排序 (本文重点是J M M
,所以不会对volatile
做过多的解读)。
三大个性的保障
个性 | volatile | synchronized | Lock | Atomic |
---|---|---|---|---|
可见性 | 能够保障 | 能够保障 | 能够保障 | 能够保障 |
原子性 | 无奈保障 | 能够保障 | 能够保障 | 能够保障 |
有序性 | 肯定水平保障 | 能够保障 | 能够保障 | 无奈保障 |
对于我
这里是阿星,一个酷爱技术的 Java 程序猿,公众号 「程序猿阿星」 里将会定期分享操作系统、计算机网络、Java、分布式、数据库等精品原创文章,2021,与您在 Be Better 的路上独特成长!。
非常感谢各位小哥哥小姐姐们能 看到这里,原创不易,文章有帮忙能够「点个赞」或「分享与评论」,都是反对(莫要白嫖)!
愿你我都能奔赴在各自想去的路上,咱们下篇文章见!
交个敌人