Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Java后端开发岗必备技能:Java并发中的内存模型

Java后端开发岗必备技能:Java并发中的内存模型

作者头像
欧阳愠斐
修改于 2019-05-14 02:08:05
修改于 2019-05-14 02:08:05
38700
代码可运行
举报
文章被收录于专栏:架构专栏架构专栏
运行总次数:0
代码可运行

原文链接Java后端开发岗必备技能:Java并发中的内存模型

JMM通过构建一个统一的内存模型来屏蔽掉不同硬件平台和不同操作系统之间的差异,让Java开发者无需关注不同平台之间的差异,达到一次编译,随处运行的目的,这也正是Java的设计目的之一。

CPU和内存

在讲JMM之前,我想先和大家聊聊硬件层面的东西。大家应该都知道执行运算操作的CPU本身是不具备存储能力的,它只负责根据指令对传递进来的数据做相应的运算,而数据存储这一任务则交给内存去完成。虽然内存的运行速度虽然比起硬盘快非常多,但是和3GHZ,4GHZ,甚至5GHZ的CPU比起来还是太慢了,在CPU的眼中,内存运行的速度简直就是弟弟中的弟弟,等内存进行一次读写操作,CPU能思考成百上千次人生了:grin:。但是CPU的运算能力是紧缺资源啊,可不能这么白白浪费了,所以得想办法解决这一个问题。

没有什么问题是一个缓存不能解决的,如果有,那就再加一个缓存 ——鲁迅:反正我没说这句话

所以人们就想到了给CPU增加一个高速缓存(为什么是加高速缓存而不是给内存提高速度就牵扯到硬件成本问题了)来解决这个问题,比如像博主用的Intel的I9 9900k CPU就带了高达16M的三级缓存,所以硬件上的内存模型大概如下图所示。

如图可以很清楚的看到,在CPU内部构建了一到多层的缓存,并且其中的L1 Cache是CPU内核心独有的,不同的Core之间是不能共享的,而L2 Cache则是所有的核心共享。简单来说就是CPU在读取一个数据时会先去最近的Cache层级上读取,如果找不到则会去下一个层级寻找,都找不到的话就会从内存中去加载,而如果Cache中能拿到所需要的数据就不会去内存读取。在将数据写回的时候也会先写入Cache中,等待合适的时机再写入到内存中(其中有一个细节就是缓存行的问题,关于这部分内容放在文章结尾)。而由于存在多个cache层级,并且部分cache还不能够被共享,所以会存在内存可见性的问题。

举个简单的例子: 假设现在存在两个Core,分别是CoreA和CoreB并且他们都拥有属于自己的L1Chace和共用的L2Cache。同时有一个变量X的值为1,该变量已经被加载在L2Cahce上。此时CoreA执行运算需要用到变量X,先去L1Cache寻找,未命中,继续在L2Cache寻找,命中成功,将X=1载入L1Cahce,再经过一系列运算后将X修改为2并写入L1Cache。于此同时CoreB刚好也需要X来进行运算,此时他去自己的L1Cahce寻找,未命中,继续L2Cache寻找,命中成功,将X=1载入自己的L1Cache。此时就出现了问题,CoreA明明已经将X的值修改为2了,CoreB读取到的依然是X=1,这就是内存可见性问题。

看到这里的小伙伴们可能要问了,博主你啥情况啊,你这写的渐渐忘记标题了啊,说好了Java内存模型,你扯这么多硬件上的问题干啥啊?(╯‵□′)╯︵┻━┻

Java中的主内存和工作内存

小伙伴们别着急,其实JMM和上面的硬件层次上的模型很像,不信看下面的图片

怎么样,是不是看起来很像,可以简单的理解为线程的工作内存就是CPU里Core独占的L1Cahce,而主内存就是共享的L2Cache。所以上述的内存一致性问题也会在JMM中存在,而JMM就需要制定一些列的规则来保证内存一致性,这也是Java多线程并发的一个疑难点,那么JMM制定了哪些规则呢?

内存间交互操作 首先是主内存的工作内存之间的交互协议,具体来说定义了以下几个操作(并且保证这几个操作都是原子性的):

  • lock (锁定)作用于主内存的变量,将一个变量标识为一个线程独占状态
  • unlock(解锁)作用于主内存的变量,将一个处于锁定状态的变量释放出来,释放之后才能被其他线程锁定
  • read(读取)作用于主内存的变量,将一个变量的值从主内存传输到线程工作内存中,便于之后的load操作使用
  • load(载入)作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用)作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  • assign(赋值)作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储)作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  • write(写入)作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

同时还规定了执行上述八个操作时必须遵循以下规则:

  • 如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
  • 不允许read和load、store和write操作之一单独出现
  • 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
  • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

(上述部分参考并引用《深入理解Java虚拟机》中的内容)

volatile(能够保证内存可见性和禁止指令重排序)

对于volatile修饰的变量,JMM对其有一些特殊的规定。

内存可见性

往简单来说volatile关键字可以理解为,有一个volatile修饰的变量x,当一个线程需要使用该变量的时候,直接从主内存中读取,而当一个线程修改该变量的值时,直接写入到主内存中。根据之前的分析我们能得出具备这些特性的volatile能够保证一个变量的内存可见性和内存一致性。

指令重排序

指令重排序是一个大部分CPU都有的操作,同时JVM在运行时也会存在指令重排序的操作。 简单举个:chestnut:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
      private void test(){
        int a,b,c;//1
        a=1;//2
        b=3;//3
        c=a+b;//4
    }

假设有上面这么一个方法,内部有这4行代码。那么JVM可能会对其进行指令重排序,而指令重排序的规定则是as-if-serial 不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。根据这一规定,编译器和处理器不会对有依赖关系的指令重排序,但是对没有依赖的指令则可能会进行重排序。放在上面的例子里面就是,第1行代码和2,3,4行代码是有依赖关系的,所以第一行代码的指令必须排在2,3,4之前,因为不可能对一个未定义的变量进行赋值操作。而第2,3行代码之间并没有相互依赖关系,所以此处可能会发生指令重排序,先执行3,再执行2。而最后的第4行代码和之前的3行代码都有依赖关系,所以他一定会放在最后执行。

既然JVM特别指出指令重排序只在单线程下和未排序的效果一致,那是否表示在多线程下会存在一些问题呢? 答案是肯定的,多线程下指令重排序会带来一些意想不到的结果。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 int a=0;
    //flag作为一个标识符,标识是否写入完成
    boolean flag = false;
    public void writer(){
        a=10;//1
        flag=true;//2
    }
    public void reader(){
        if (flag)
            System.out.println("a:"+a);
    }

假设存在一个类,他有上述部分的field和method,该类在设计上以flag作为写入是否完成的标志,在单线程下这并不会存在问题。而此时有两个线程分别执行writer和reader方法,暂时不考虑内存可见性的问题,假设对a和flag的写入,是立即被其他线程所知晓的,这个时候大家觉得输出a的值为多少?10?

即使不考虑内存可见性,此时a的值还是有可能会输出0,这就是指令重排序带来的问题。在上述代码中注释1和2处的代码是没有依赖关系的,在单线程下先执行1还是2都没有任何问题,根据as-if-serial 原则此时就可能会发生指令重排序。

而volatile关键字可以禁止指令重排序。

long,double的问题

我们都知道JMM定义的8个主内存和工作内存之间的操作都是具备原子性的,但是对long和double这两个64位的数据类型有一些例外。

允许虚拟机将没有被volatile修饰的long和double的64数据的读写操作划分为两次32位的读写操作,即不要求虚拟机保证对他们的load ,store,read,write四个操作的原子性。 但是大部分的虚拟机实现都保证了这四个操作的原子性的,所以大部分时候我们都不需要刻意的对long,double对象使用volatile修饰。

性能问题

volatile是Java提供的保证内存可见性的最轻量级操作,比起重量级的synchronized能快上不少,但是具体能快多少这部分没办法量化。而我们可以知道的是volatile修饰的变量读操作的性能消耗几乎和普通变量相差无几,而写操作则会慢上一些。所以当volatile能解决我们的问题的时候(内存可见性和禁止指令重排序),我们应该优先选择使用volatile而不是锁。

synchronized的内存语义

简单概括就是

当程序进入synchronized块时,把在synchronized块中用到的变量从工作内存中清楚,这样在需要访问这些变量的时候会重新从主内存中获取。当程序退出synchronized块时,把对块中恭喜变量的修改刷新到主内存。 如此依赖synchronized也能保证了内存的可见性。

final的内存语义

final也能保证内存的可见性

被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把this引用传递出去,那么其他线程中就能看见final字段的值。

后记之CPU缓存行和伪共享

什么是伪共享

根据前面的文章,我们知道CPU和Memory之间是有Cache的,而Cache内部是按行存储的,行拥有固定的大小,这些行被称为缓存行。 当CPU访问的某个变量不在Cache中时,就会去内存里获取,并将该变量所在内存的一个缓存行大小的数据读入Cache中。由于一次读取并不是单个对象而是一整个缓存行,所以可能会存在多个变量被读入一个缓存行中。而一个缓存行只能同时被一个线程操作,所以当多个线程同时修改一个缓存行里的多个变量时会造成其他线程等待从而带来性能损耗(但是在单线程情况下,伪共享反而会提升性能,因为一次性可能会缓存多个变量,节省后续变量的读取时间)。

如何避免伪共享

在Java8之后可以使用JDK提供的@sun.misc.Contended注解来解决伪共享,像Thread中的threadLocalRandom 字段就使用了这个注解。

那么架构师掌握的技能又有哪些呢?

前面我写的文章《揭秘阿里Java架构师背后的技术体系支撑(详细分层,建议收藏)》中有详细讲解。

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
并发编程-02并发基础CPU多级缓存和Java内存模型JMM
CPU的频率非常快,主存Main Memory跟不上。CPU缓存是CPU与内存之间的临时数据交换器,为了解决CPU运行处理速度与内存读写速度不匹配的矛盾——缓存的速度比内存的速度快多了。
小小工匠
2021/08/17
5170
死磕并发:Java内存模型
首先我们在了解java内存模型之前先看一下计算机内存模型,理解了计算机内存模型的话后面在看JMM就会简单的多,上篇文章我是直接写的。
乱敲代码
2019/09/17
4550
死磕并发:Java内存模型
一文详解JMM(Java 内存模型)
要想要理解透彻JMM(Java内存模型),首先我们要从CPU缓存模型和指令重排序讲起!
会呼吸的Coder
2022/12/02
1K0
一文详解JMM(Java 内存模型)
Java内存模型
Java 内存模型试图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。
Java架构师必看
2021/07/14
3430
万丈高楼平地起—Java并发的基石JMM(Java内存模型)
顺序一致性模型可以保证并发编程的特性不被破坏,为多线程程序提供了极强的 内存一致性保证
淇妙小屋
2022/03/30
2530
万丈高楼平地起—Java并发的基石JMM(Java内存模型)
JMM Java内存模型
Java采用内存共享的模式来实现线程之间的通信。编译器和处理器可以对程序进行重排序优化处理,但是需要遵守一些规则,不能随意重排序。
luoxn28
2020/11/05
5480
JAVA系列之内存模型(JMM)
Java内存模型是在硬件内存模型上的更高层的抽象,它屏蔽了各种硬件和操作系统访问的差异性,保证了Java程序在各种平台下对内存的访问都能达到一致的效果。 Java内存模型是不可见的,它并不是一个真实的东西,它只是一个概念、一个规范。
夕阳也是醉了
2023/10/16
2220
JAVA系列之内存模型(JMM)
面试官:什么是Java内存模型?
当问到 Java 内存模型的时候,一定要注意,Java 内存模型(Java Memory Model,JMM)它和 JVM 内存布局(JVM 运行时数据区域)是不一样的,它们是两个完全不同的概念。
磊哥
2024/02/22
5450
Java 内存模型
物理机遇到的并发问题与虚拟机中的情况有不少相似之处,物理机对并发的处理方案对于虚拟机的实现也有相当大的参考意义。
静默虚空
2019/12/30
9030
Java 内存模型
lsp都要看的内存模型
JMM(Java Memory Model):全称Java内存模型。它定义了**Java虚拟机在计算机内存中的工作方式**。它是一套规范,并不真实存在。它包括三个点:原子性,可见性,有序性
石的三次方
2021/01/05
6730
Java高并发:Java内存模型
因为缓存脏数据写回主内存一般采用的是写回法,而非直写法,所以缓存和主存之间会有数据一致性问题。
冰寒火
2023/03/01
8950
Java 并发基础之内存模型
关于 Java 并发也算是写了好几篇文章了,本文将介绍一些比较基础的内容,注意,阅读本文需要一定的并发基础。
技术zhai
2022/01/21
2110
为了研究Java内存模型(JMM),我又学了一点汇编指令
CPU都有自己的L1、L2、L3缓存,CPU会将常用的数据,从主内存同步到缓存中,以此来提高数据的访问速度。如果CPU修改了缓存中的数据,就会从缓存更新到主内存中。
叫我阿柒啊
2024/03/08
3780
为了研究Java内存模型(JMM),我又学了一点汇编指令
【Java】【并发编程】详解Java内存模型
Java内存模型即 Java Menory Model,简称JMM。JMM定义了Java虚拟机(JVM)在计算机内存(RAM)中的工作方法。JVM是整个计算机虚拟模型,所以JMM隶属于JVM的。
玖柒的小窝
2021/09/24
2K0
【Java】【并发编程】详解Java内存模型
不会Java内存模型,就先别扯什么熟悉并发编程
前两天看到同学和我显摆他们公司配的电脑多好多好,我默默打开了自己的电脑,酷睿 i7-4770,也不是不够用嘛,4 核 8 线程的 CPU,也是杠杠的。
乔戈里
2020/03/25
4100
Java内存模型
java内存模型(JMM)全称为Java Memory Model,是java虚拟机为了java程序能够正常运行而制定的一套规范,规范中规定了JVM中的数据如何与RAM的数据进行交互。
诺浅
2020/08/19
5910
java内存模型_简述java内存模型
  JMM即为JAVA 内存模型(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问逻辑有一定的差异,结果就是当你的代码在某个系统环境下运行良好,并且线程安全,但是换了个系统就出现各种问题。Java内存模型,就是为了屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果。JMM从java 5开始的JSR-133发布后,已经成熟和完善起来。
全栈程序员站长
2022/10/03
1.2K0
java内存模型_简述java内存模型
全面理解Java内存模型
Java内存模型即Java Memory Model,简称JMM。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。
全栈程序员站长
2022/07/22
4350
全面理解Java内存模型
JVM暴力突破之JMM内存模型
JMM规定了内存主要划分为主内存和工作内存两种。此处的主内存和工作内存跟JVM内存划分(堆、栈、方法区)是在不同的层次上进行的,如果非要对应起来,主内存对应的是Java堆中的对象实例部分,工作内存对应的是栈中的部分区域,从更底层的来说,主内存对应的是硬件的物理内存,工作内存对应的是寄存器和高速缓存。具体如图所示:
我就是马云飞
2021/02/26
5860
JVM暴力突破之JMM内存模型
并发编程系列之什么是Java内存模型?
Java内存模型简称JMM(Java Memory Model),JMM是和多线程并发相关的一组规范。各个jvm实现都要遵循这个JMM规范。才能保证Java代码在不同虚拟机顺利运行。因此,JMM 与处理器、缓存、并发、编译器有关。它解决了CPU 多级缓存、处理器优化、指令重排等导致的结果不可预期的问题。
SmileNicky
2021/11/24
2.2K0
并发编程系列之什么是Java内存模型?
相关推荐
并发编程-02并发基础CPU多级缓存和Java内存模型JMM
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验