前言

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 = 10int 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做过多的解读)。

三大个性的保障

个性volatilesynchronizedLockAtomic
可见性能够保障能够保障能够保障能够保障
原子性无奈保障能够保障能够保障能够保障
有序性肯定水平保障能够保障能够保障无奈保障

对于我

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

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

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

交个敌人