速读摘要文章源自JAVA秀-https://www.javaxiu.com/5148.html
Java并发编程系列开坑了,Java并发编程可以说是中高级研发工程师的必备素养,也是中高级岗位面试必问的问题,本系列就是为了带读者们系统的一步一步击破Java并发编程各个难点,打破屏障,在面试中所向披靡,拿到心仪的offer,Java并发编程系列文章依然采用图文并茂的风格,让小白也能秒懂。C P U运行时,会将指令与数据从主存复制到缓存层,后续的读写与运算都是基于缓存层的指令与数据,运算结束后,再将结果从缓存层写回主存。文章源自JAVA秀-https://www.javaxiu.com/5148.html
原文约 3252 字 | 图片 16 张 | 建议阅读 7 分钟 | 评价反馈文章源自JAVA秀-https://www.javaxiu.com/5148.html
小白也能看懂的Java内存模型
程序IT圈 文章源自JAVA秀-https://www.javaxiu.com/5148.html
以下文章来源于程序猿阿星,作者点击关注 ?文章源自JAVA秀-https://www.javaxiu.com/5148.html
文章源自JAVA秀-https://www.javaxiu.com/5148.html 文章源自JAVA秀-https://www.javaxiu.com/5148.html 程序猿阿星文章源自JAVA秀-https://www.javaxiu.com/5148.html 一起成长进阶!专注技术原理、源码,通过图解方式输出技术,这里将会分享操作系统、计算机网络、Java、分布式、数据库等精品原创文章文章源自JAVA秀-https://www.javaxiu.com/5148.html
前言
Java并发编程系列开坑了,Java并发编程可以说是中高级研发工程师的必备素养,也是中高级岗位面试必问的问题,本系列就是为了带读者们系统的一步一步击破Java并发编程各个难点,打破屏障,在面试中所向披靡,拿到心仪的offer,Java并发编程系列文章依然采用图文并茂的风格,让小白也能秒懂。文章源自JAVA秀-https://www.javaxiu.com/5148.html
Java内存模型(Java Memory Model
)简称J M M
,作为Java并发编程系列的开篇,它是Java并发编程的基础知识,理解它能让你更好的明白线程安全到底是怎么一回事。文章源自JAVA秀-https://www.javaxiu.com/5148.html
内容大纲
文章源自JAVA秀-https://www.javaxiu.com/5148.html
硬件内存模型
程序是指令与数据的集合,计算机执行程序时,是C P U
在执行每条指令,因为C P U
要从内存读指令,又要根据指令指示去内存读写数据做运算,所以执行指令就免不了与内存打交道,早期内存读写速度与C P U
处理速度差距不大,倒没什么问题。文章源自JAVA秀-https://www.javaxiu.com/5148.html
C P U缓存
随着C P U
技术快速发展,C P U
的速度越来越快,内存却没有太大的变化,导致内存的读写(IO
)速度与C P U
的处理速度差距越来越大,为了解决这个问题,引入了缓存(Cache
)的设计,在C P U
与内存之间加上缓存层,这里的缓存层就是指C P U
内的寄存器与高速缓存(L1,L2,L3
)文章源自JAVA秀-https://www.javaxiu.com/5148.html
文章源自JAVA秀-https://www.javaxiu.com/5148.html
从上图中可以看出,寄存器最快,主内最慢,越快的存储空间越小,离C P U
越近,相反存储空间越大速度越慢,离C P U
越远。文章源自JAVA秀-https://www.javaxiu.com/5148.html
C P U如何与内存交互
C P U
运行时,会将指令与数据从主存复制到缓存层,后续的读写与运算都是基于缓存层的指令与数据,运算结束后,再将结果从缓存层写回主存。文章源自JAVA秀-https://www.javaxiu.com/5148.html
文章源自JAVA秀-https://www.javaxiu.com/5148.html
上图可以看出,C P U
基本都是在和缓存层打交道,采用缓存设计弥补主存与C P U
处理速度的差距,这种设计不仅仅体现在硬件层面,在日常开发中,那些并发量高的业务场景都能看到,但是凡事都有利弊,缓存虽然加快了速度,同样也带来了在多线程场景存在的缓存一致性问题,关于缓存一致性问题后面会说,这里大家留个印象。文章源自JAVA秀-https://www.javaxiu.com/5148.html
Java内存模型
Java内存模型(Java Memory Model,J M M
),后续都以J M M
简称,J M M
是建立在硬件内存模型基础上的抽象模型,并不是物理上的内存划分,简单说,为了使Java
虚拟机(Java Virtual Machine,J V M
)在各平台下达到一致的内存交互效果,需要屏蔽下游不同硬件模型的交互差异,统一规范,为上游提供统一的使用接口。文章源自JAVA秀-https://www.javaxiu.com/5148.html
J M M
是保证J V M
在各平台下对计算机内存的交互都能保证效果一致的机制及规范。文章源自JAVA秀-https://www.javaxiu.com/5148.html
文章源自JAVA秀-https://www.javaxiu.com/5148.html
抽象结构
J M M
抽象结构划分为线程本地缓存与主存,每个线程均有自己的本地缓存,本地缓存是线程私有的,主存则是计算机内存,它是共享的。文章源自JAVA秀-https://www.javaxiu.com/5148.html
文章源自JAVA秀-https://www.javaxiu.com/5148.html
不难发现J M M
与硬件内存模型差别不大,可以简单的把线程类比成Core核心,线程本地缓存类比成缓存层,如下图所示文章源自JAVA秀-https://www.javaxiu.com/5148.html
文章源自JAVA秀-https://www.javaxiu.com/5148.html
虽然内存交互规范好了,但是多线程场景必然存在线程安全问题(竞争共享资源),为了使多线程能正确的同步执行,就需要保证并发的三大特性可见性、原子性、有序性。文章源自JAVA秀-https://www.javaxiu.com/5148.html
可见性
当一个线程修改了共享变量的值,其他线程能够立即得知这个修改,这就是可见性,如果无法保证,就会出现缓存一致性的问题,J M M
规定,所有的变量都放在主存中,当线程使用变量时,先从缓存中获取,缓存未命中,再从主存复制到缓存,最终导致线程操作的都是自己缓存中的变量。文章源自JAVA秀-https://www.javaxiu.com/5148.html
文章源自JAVA秀-https://www.javaxiu.com/5148.html
线程A执行流程文章源自JAVA秀-https://www.javaxiu.com/5148.html
线程
A
从缓存获取变量a
文章源自JAVA秀-https://www.javaxiu.com/5148.html缓存未命中,从主存复制到缓存,此时
a
是0
文章源自JAVA秀-https://www.javaxiu.com/5148.html线程
A
获取变量a
,执行计算文章源自JAVA秀-https://www.javaxiu.com/5148.html计算结果
1
,写入缓存文章源自JAVA秀-https://www.javaxiu.com/5148.html计算结果
1
,写入主存文章源自JAVA秀-https://www.javaxiu.com/5148.html
线程B执行流程文章源自JAVA秀-https://www.javaxiu.com/5148.html
线程
B
从缓存获取变量a
文章源自JAVA秀-https://www.javaxiu.com/5148.html缓存未命中,从主存复制到缓存,此时
a
是1
文章源自JAVA秀-https://www.javaxiu.com/5148.html线程
B
获取变量a,执行计算文章源自JAVA秀-https://www.javaxiu.com/5148.html计算结果
2
,写入缓存文章源自JAVA秀-https://www.javaxiu.com/5148.html计算结果
2
,写入主存文章源自JAVA秀-https://www.javaxiu.com/5148.html
A
、B
两个线程执行完后,线程A
与线程B
缓存数据不一致,这就是缓存一致性问题,一个是1
,另一个是2
,如果线程A
再进行一次+1
操作,写入主存的还是2
,也就是说两个线程对a
共进行了3
次+1
,期望的结果是3
,最终得到的结果却是2
。文章源自JAVA秀-https://www.javaxiu.com/5148.html
解决缓存一致性问题,就要保证可见性,思路也很简单,变量写入主存后,把其他线程缓存的该变量清空,这样其他线程缓存未命中,就会去主存加载。文章源自JAVA秀-https://www.javaxiu.com/5148.html
文章源自JAVA秀-https://www.javaxiu.com/5148.html
线程A执行流程文章源自JAVA秀-https://www.javaxiu.com/5148.html
线程
A
从缓存获取变量a
文章源自JAVA秀-https://www.javaxiu.com/5148.html缓存未命中,从主存复制到缓存,此时
a
是0
文章源自JAVA秀-https://www.javaxiu.com/5148.html线程
A
获取变量a
,执行计算文章源自JAVA秀-https://www.javaxiu.com/5148.html计算结果
1
,写入缓存文章源自JAVA秀-https://www.javaxiu.com/5148.html计算结果
1
,写入主存,并清空线程B
缓存a
变量文章源自JAVA秀-https://www.javaxiu.com/5148.html
线程B执行流程文章源自JAVA秀-https://www.javaxiu.com/5148.html
线程
B
从缓存获取变量a
文章源自JAVA秀-https://www.javaxiu.com/5148.html缓存未命中,从主存复制到缓存,此时
a
是1
文章源自JAVA秀-https://www.javaxiu.com/5148.html线程
B
获取变量a,执行计算文章源自JAVA秀-https://www.javaxiu.com/5148.html计算结果
2
,写入缓存文章源自JAVA秀-https://www.javaxiu.com/5148.html计算结果
2
,写入主存,并清空线程A
缓存a
变量文章源自JAVA秀-https://www.javaxiu.com/5148.html
A
、B
两个线程执行完后,线程A
缓存是空的,此时线程A再进行一次+1
操作,会从主存加载(先从缓存中获取,缓存未命中,再从主存复制到缓存)得到2
,最后写入主存的是3
,Java
中提供了volatile
修饰变量保证可见性(本文重点是J M M
,所以不会对volatile
做过多的解读)。文章源自JAVA秀-https://www.javaxiu.com/5148.html
看似问题都解决了,然而上面描述的场景是建立在理想情况(线程有序的执行),实际中线程可能是并发(交替执行),也可能是并行,只保证可见性仍然会有问题,所以还需要保证原子性。文章源自JAVA秀-https://www.javaxiu.com/5148.html
原子性
原子性是指一个或者多个操作在C P U
执行的过程中不被中断的特性,要么执行,要不执行,不能执行到一半,为了直观的了解什么是原子性,看看下面这段代码文章源自JAVA秀-https://www.javaxiu.com/5148.html
int a=0;a++;
原子性操作:
int a=0
只有一步操作,就是赋值文章源自JAVA秀-https://www.javaxiu.com/5148.html非原子操作:
a++
有三步操作,读取值、计算、赋值文章源自JAVA秀-https://www.javaxiu.com/5148.html
如果多线程场景进行a++
操作,仅保证可见性,没有保证原子性,同样会出现问题。文章源自JAVA秀-https://www.javaxiu.com/5148.html
文章源自JAVA秀-https://www.javaxiu.com/5148.html
并发场景(线程交替执行)文章源自JAVA秀-https://www.javaxiu.com/5148.html
线程
A
读取变量a
到缓存,a
是0
文章源自JAVA秀-https://www.javaxiu.com/5148.html进行
+1
运算得到结果1
文章源自JAVA秀-https://www.javaxiu.com/5148.html切换到
B
线程文章源自JAVA秀-https://www.javaxiu.com/5148.htmlB
线程执行完整个流程,a=1
写入主存文章源自JAVA秀-https://www.javaxiu.com/5148.html线程
A
恢复执行,把结果a=1
写入缓存与主存文章源自JAVA秀-https://www.javaxiu.com/5148.html最终结果错误文章源自JAVA秀-https://www.javaxiu.com/5148.html
并行场(线程同时执行)文章源自JAVA秀-https://www.javaxiu.com/5148.html
线程
A
与线程B
同时执行,可能线程A
执行运算+1
的时候,线程B
就已经全部执行完成,也可能两个线程同时计算完,同时写入,不管是那种,结果都是错误的。文章源自JAVA秀-https://www.javaxiu.com/5148.html
为了解决此问题,只要把多个操作变成一步操作,即保证原子性。文章源自JAVA秀-https://www.javaxiu.com/5148.html
文章源自JAVA秀-https://www.javaxiu.com/5148.html
Java
中提供了synchronized
(同时满足有序性、原子性、可见性)可以保证结果的原子性(注意这里的描述),synchronized
保证原子性的原理很简单,因为synchronized
可以对代码片段上锁,防止多个线程并发执行同一段代码(本文重点是J M M
,所以不会对synchronized
做过多的解读)。文章源自JAVA秀-https://www.javaxiu.com/5148.html
文章源自JAVA秀-https://www.javaxiu.com/5148.html
并发场景(线程A
与线程B
交替执行)文章源自JAVA秀-https://www.javaxiu.com/5148.html
线程
A
获取锁成功文章源自JAVA秀-https://www.javaxiu.com/5148.html线程
A
读取变量a
到缓存,进行+1
运算得到结果1
文章源自JAVA秀-https://www.javaxiu.com/5148.html此时切换到了
B
线程文章源自JAVA秀-https://www.javaxiu.com/5148.html线程
B
获取锁失败,阻塞等待文章源自JAVA秀-https://www.javaxiu.com/5148.html切换回线程
A
文章源自JAVA秀-https://www.javaxiu.com/5148.html线程
A
执行完所有流程,主存a=1
文章源自JAVA秀-https://www.javaxiu.com/5148.html线程A释放锁成功,通知线程
B
获取锁文章源自JAVA秀-https://www.javaxiu.com/5148.html线程B获取锁成功,读取变量
a
到缓存,此时a=1
文章源自JAVA秀-https://www.javaxiu.com/5148.html线程B执行完所有流程,主存
a=2
文章源自JAVA秀-https://www.javaxiu.com/5148.html线程B释放锁成功文章源自JAVA秀-https://www.javaxiu.com/5148.html
并行场景文章源自JAVA秀-https://www.javaxiu.com/5148.html
线程
A
获取锁成功文章源自JAVA秀-https://www.javaxiu.com/5148.html线程
B
获取锁失败,阻塞等待文章源自JAVA秀-https://www.javaxiu.com/5148.html线程
A
读取变量a
到缓存,进行+1
运算得到结果1
文章源自JAVA秀-https://www.javaxiu.com/5148.html线程
A
执行完所有流程,主存a=1
文章源自JAVA秀-https://www.javaxiu.com/5148.html线程
A
释放锁成功,通知线程B
获取锁文章源自JAVA秀-https://www.javaxiu.com/5148.html线程
B
获取锁成功,读取变量a
到缓存,此时a=1
文章源自JAVA秀-https://www.javaxiu.com/5148.html线程
B
执行完所有流程,主存a=2
文章源自JAVA秀-https://www.javaxiu.com/5148.html线程
B
释放锁成功文章源自JAVA秀-https://www.javaxiu.com/5148.html
synchronized
对共享资源代码段上锁,达到互斥效果,天然的解决了无法保证原子性、可见性、有序性带来的问题。文章源自JAVA秀-https://www.javaxiu.com/5148.html
虽然在并行场A
线程还是被中断了,切换到了B
线程,但它依然需要等待A
线程执行完毕,才能继续,所以结果的原子性得到了保证。文章源自JAVA秀-https://www.javaxiu.com/5148.html
有序性
在日常搬砖写代码时,可能大家都以为,程序运行时就是按照编写顺序执行的,但实际上不是这样,编译器和处理器为了优化性能,会对代码做重排,所以语句实际执行的先后顺序与输入的代码顺序可能一致,这就是指令重排序。文章源自JAVA秀-https://www.javaxiu.com/5148.html
可能读者们会有疑问“指令重排为什么能优化性能?”,其实C P U
会对重排后的指令做并行执行,达到优化性能的效果。文章源自JAVA秀-https://www.javaxiu.com/5148.html
重排序前的指令文章源自JAVA秀-https://www.javaxiu.com/5148.html
文章源自JAVA秀-https://www.javaxiu.com/5148.html
重排序后的指令文章源自JAVA秀-https://www.javaxiu.com/5148.html
文章源自JAVA秀-https://www.javaxiu.com/5148.html
重排序后,对a
操作的指令发生了改变,节省了一次Load a
和Store a
,达到性能优化效果,这就是重排序带来的好处。文章源自JAVA秀-https://www.javaxiu.com/5148.html
重排遵循as-if-serial
原则,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果(即不管怎么重排序,单线程程序的执行结果不能被改变),下面这种情况,就属于数据依赖。文章源自JAVA秀-https://www.javaxiu.com/5148.html
int i = 10int j = 10//这就是数据依赖,int i 与 int j 不能排到 int c下面去int c = i + j
但也仅仅只是针对单线程,多线程场景可没这种保证,假设A、B
两个线程,线程A
代码段无数据依赖,线程B
依赖线程A
的结果,如下图(假设保证了可见性)文章源自JAVA秀-https://www.javaxiu.com/5148.html
文章源自JAVA秀-https://www.javaxiu.com/5148.html
禁止重排场景(i默认0)文章源自JAVA秀-https://www.javaxiu.com/5148.html
线程
A
执行i = 10
文章源自JAVA秀-https://www.javaxiu.com/5148.html线程
A
执行b = true
文章源自JAVA秀-https://www.javaxiu.com/5148.html线程
B
执行if( b )
通过验证文章源自JAVA秀-https://www.javaxiu.com/5148.html线程
B
执行i = i + 10
文章源自JAVA秀-https://www.javaxiu.com/5148.html最终结果
i
是20
文章源自JAVA秀-https://www.javaxiu.com/5148.html
重排场景(i默认0)文章源自JAVA秀-https://www.javaxiu.com/5148.html
线程
A
执行b = true
文章源自JAVA秀-https://www.javaxiu.com/5148.html线程
B
执行if( b )
通过验证文章源自JAVA秀-https://www.javaxiu.com/5148.html线程
B
执行i = i + 10
文章源自JAVA秀-https://www.javaxiu.com/5148.html线程
A
执行i = 10
文章源自JAVA秀-https://www.javaxiu.com/5148.html最终结果
i
是10
文章源自JAVA秀-https://www.javaxiu.com/5148.html
为解决重排序,使用Java提供的volatile
修饰变量同时保证可见性、有序性,被volatile
修饰的变量会加上内存屏障禁止排序(本文重点是J M M
,所以不会对volatile
做过多的解读)。文章源自JAVA秀-https://www.javaxiu.com/5148.html
三大特性的保证
特性 | volatile | synchronized | Lock | Atomic |
---|---|---|---|---|
可见性 | 可以保证 | 可以保证 | 可以保证 | 可以保证 |
原子性 | 无法保证 | 可以保证 | 可以保证 | 可以保证 |
有序性 | 一定程度保证 | 可以保证 | 可以保证 | 无法保证 |
致谢
非常感谢各位小哥哥小姐姐们能 看到这里,原创不易,文章有帮助可以「点个赞」或「分享与评论」,都是支持(莫要白嫖)!文章源自JAVA秀-https://www.javaxiu.com/5148.html
愿你我都能奔赴在各自想去的路上,我们下篇文章见!文章源自JAVA秀-https://www.javaxiu.com/5148.html
最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。获取方式:关注公众号并回复 java 领取,更多内容陆续奉上。文章源自JAVA秀-https://www.javaxiu.com/5148.html
明天见(。・ω・。)ノ♡文章源自JAVA秀-https://www.javaxiu.com/5148.html

评论