热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

全面解析Java虚拟机:内存模型深度剖析

要了解Java内存模型,首先我们要了解什么是Java内存模型,它有什么作用? 描述Java内存模型(简称:JMM)的规范提案JSR-133标题《Java Memory Model and Thread

要了解Java内存模型,首先我们要了解什么是Java内存模型,它有什么作用?
描述Java内存模型(简称:JMM)的规范提案JSR-133标题《Java Memory Model and Thread Specification》,通过这个标题,可以看出JMM是和线程相关的规范。此规范地指定的 JMM Web Site 上对规范的说明如下:

The Java Memory Model defines how threads interact through memory.

通过以上描述,说明JMM规范主要是解决在多线程场景下线程间如何通信。

硬件内存架构

要了解JMM,我们先来从硬件角度,看看多核CPU场景下,多线程程序会存在什么问题。
在这里插入图片描述

如上图所示,在多核(多CPU)硬件架构中,系统中有两个CPU,分布运行了一个线程,对象obj保存在主内存(RAM)中。由于RAM的速度远低于CPU,为了加快数据的访问,当CPU(线程)需要使用obj对象时,会预先把obj对象加载到CPU的缓存(CPU Cache)中,处理完毕后,再把对obj对象的更新回写到到RAM。
每个CPU有自己独立的缓存,一个CPU无法访问其他CPU的缓存,也就是CPU间无法直接交换数据,CPU间所有的数据交换都需要借助主内存来完成。

假设线程执行的是 +1 操作。在上图示例中,两个线程并发执行。初始状态,主内存中obj.num=1;线程1先读取了obj对象,并执行+1操作,结果obj.num=2;在线程1的修改还未从CPU缓存回写到主内存的时候,线程2从主内存中读取了obj对象,此时线程2读取到的obj.num=1;此后,线程1和线程2分别把obj回写到主内存;按正常业务逻辑,obj.num被+1了两次,结果应该是3,但上述情况,最终主内存中obj.num=2。这是因为两个线程对数据并发访问冲突导致线程读到的数据不一致。

Java内存模型

Java是平台无关的语言,为了实现跨平台运行,Java虚拟机(JVM)上运行的是Java字节码(Java bytecode)。Java内存模型(Java Memory Model,JMM)是Java虚拟机规范定义的,用来屏蔽掉Java程序在各种不同的硬件和操作系统对内存的访问的差异,实现Java程序在各种不同的平台上都能达到内存访问的一致性。和硬件内存架构类似,JMM把内存分为主内存工作内存,主内存由所有线程共享,工作内存为线程私有。
JMM规范主要定义程序变量操作的规则,规范中定义的主内存、工作内存的概念和JVM运行时内存分区中定义的堆、栈区域不是同一纬度的概念,不能互相对应,不过为了便于理解,可把主内存类比为堆,工作内存类比为栈。

虽然工作内存和栈可以类比,但两者是不同的概念。
JMM管理的程序变量,主要是指在对象实例字段、静态字段、构成数组字段的元素等,不包括方法参数、方法局部变量等保存在栈里的变量,因为栈本身就是线程私有的,并不存在线程一致性问题。
JMM规范规定所有的变量都要在主内存中产生,而线程不允许直接操作主内存中的变量,线程需要把变量副本拷贝到工作线程中进行操作,操作完后再回写到主内存。

在这里插入图片描述

主内存
JMM规定所有的变量都必须在主内存中产生。

工作内存
JVM中每个线程都有自己的工作内存,是线程私有的,可以类比CPU的高速缓存。线程的工作内存保存了线程需要的变量在主内存中的副本。

数据交互接口

JMM中定义了8个用于主内存和工作内存见数据互操作的接口,用于在两者间传输数据,这些操作都是原子性的。

  1. lock(锁定)
    作用于主内存变量,属于互斥锁,一个变量同时只能一个线程锁定
  2. unlock(解锁)
    作用于主内存变量,lock的反操作,释放变量的锁
  3. read(读取)
    作用于主内存变量,表示把一个主内存变量的值传输到线程的工作内存,以便随后的load操作使用
  4. load(载入)
    作用于线程工作内存变量,表示把read操作从主内存中读取的变量的值放到工作内存的变量副本中
  5. use(使用)
    作用于线程工作内存变量,表示把工作内存中的一个变量的值传递给字节码指令
  6. assign(赋值)
    作用于线程工作内存变量,表示把字节码指令执行返回的结果赋值给工作内存中的变量,字节码赋值操作
  7. store(存储)
    作用于线程工作内存变量,把工作内存中的一个变量的值传递给主内存,以便随后的write操作使用
  8. write(写入)
    作用于主内存变量,把store操作从工作内存中得到的变量的值放入主内存的变量中

数据交互原则

  1. 变量只能在主内存中产生。
  2. 线程对主内存变量的操作必须在线程的工作内存中进行,不能直接操作主内存中的变量。
  3. 不同的线程之间也不能相互访问对方的工作内存。线程之间需要传递变量的值,必须通过主内存来作为中介进行传递。
  4. read和load操作、store和write必须成对使用,即:不允许从主内存中读取了变量,工作内存不接收,或者工作内存回写了变量,主内存不接收。
  5. assign操作后的变量必须回写到主内存。
  6. 不允许回写没有修改(即未assign)的变量到主内存。
  7. 一个变量同时只能被一个线程对其进行lock操作,但是同一个线程对一个变量加锁后,可以继续加锁,同时在释放锁的时候释放锁次数必须和加锁次数相同。
  8. 对变量执行lock操作,就会清空工作空间该变量的值,使用时需要重新读取;对一个变量执行unlock之前,必须先把变量同步回主内存中。

指令重排(Reordering)

计算机在执行程序时,为了提高性能,编译器和处理器会对指令做重排,再对乱序执行之后的结果进行重组,保证结果的正确性。也就是说在真正的执行过程中,指令执行的顺序并不一定按照代码的书写顺序来执行,但可以保证结果与顺序执行的结果一致,这种现象成为指令重排(Reordering),指令重排优化包括以下三种情况。

  • 编译器指令的重排
    编译器在不改变单线程程序语义的前提下,可以重新调整语句的执行顺序
  • 处理器指令级并行的重排
    现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
  • 内存系统的重排
    由于处理器使用缓存和读/写缓冲区,这使得主内存和工作内存间的数据加载和存储操作看上去可能是在乱序执行的

编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序

针对编译器重排序,JMM的编译器重排序规则会禁止volatile变量synchronizedfinal等特定指令的编译器重排序;针对处理器重排序,编译器在生成指令序列的时候会通过插入内存屏障指令来禁止某些特殊的处理器重排序

内存并发一致性原则

上述的内存并发一致性问题,在JMM中定义了三个原则来避免,分别是原子性、可见性和有序性。

原子性(Atomicity)

原子性表示不可被中断的一个或一组操作。操作一旦开始,就一直运行到结束,中间不会有任何线程切换(context switch)。

可见性(Visibility)

可见性是指多个线程访问同一个变量是,一个线程修改了变量的值后,其他线程可以立即读取到这个变量的最新值。

有序性(Ording)

指程序按代码书写时希望的顺序执行,这在指令重排后尤其重要,有序性包括单线程内执行的有序性和多线程间执行的有序性。

as-if-serial
as-if-serial语义,是指不管指令怎么重排序,单线程程序的执行结果不能被改变。遵守as-if-serial语义的编译器,指令执行顺序虽然和代码书写顺序不一致,但可以保证执行的结果是正确的。

先行发生(Happens-before)
重排后的指令,在多线程同时执行情况下,从其它线程的视角来看,被指令重排的线程执行过程是不确定的,线程间执行的可见性无法保证。happens-before概念用来指定两个操作之间的执行顺序,可以提供跨线程的内存可见性保证,其具体定义如下。

  1. 如果动作A先行于动作B发生,则动作A的执行结果对于动作B可见,而且动作A的执行顺序排在动作B之前。
  2. 先行发生并不要求重排后的指令严格按先行发生的顺序执行,只要保证先后发生的动作的结果(可见性)符合先行发生原则即可。

先行发生的具体规则如下

  1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  2. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  3. volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  4. 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  5. start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  6. join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
  7. 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
  8. 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。

一致性保证的方法

volatile变量

volatile是Java中最轻量级的同步机制,JMM对volatile变量定义了特殊的操作规则,使得变量具有同步的特性,相关规则如下。

  1. 线程对volatile变量的load和use操作必须连续出现,即变量需要使用时,必须先从主内存中读取最新值;assign和store操作也必须连续出现,即线程对变量赋值后,必须马上写入主内存。通过这两点,可以保证变量对所有线程的可见性
  2. 对volatile修饰的变量,JVM禁止指令重排优化,指令按代码顺序执行,保证代码运行的有序性

需要注意的是,虽然volatile变量可以保证对所有线程的可见性,但是并不能保证变量是线程安全的,多线程并发操作下,还是会出现文章前面出现的obj.num并发冲突的问题,这是由于变量本身 +1 操作并不是原子性的,它可以分为两个步骤,即变量加载到工作内存(read、load、use)、变量赋值后回写主内存(assign、store、write),而这两个步骤并不是原子性的。A、B两个线程的执行顺序可能是这样的:

  1. 线程A读取变量obj.num=1
  2. 线程B读取变量obj.num=1
  3. 线程A执行+1,obj.num=1+1=2,并回写到主内存
  4. 线程B执行+1,obj.num=1+1=2,并回写到主内存,此时覆盖了线程A写入主内存的值

在这种情况下,要保证线程间数据同步,就需要使用lock锁住变量,这在Java语法中,表现为 synchronized 关键字。

synchronized

JMM的lock和unlock操作,对应到字节码指令是monitorenter和monitorexit两条指令,而对应的Java代码中,就是synchronized代码块或者synchronized方法。
由于lock同时只能被一个线程获取,所以可以保证操作的原子性;另外lock会触变量重读,unlock会触发变量回写,所以可以保证操作对其他线程的可见性;另外lock保证同时只有一个线程执行对应代码快,可以保证操作的有效性。

final关键字

在JMM中,final关键字确保变量初始化安全性(initialization safety)成为可能,让不可变对象不需要同步就能安全地被访问和共享。
在JMM中,通过内存屏障禁止编译器把final域的写重排序到构造函数之外,在对象引用为任意线程可见之前,对象的final域已经被正确初始化了。
对于final域,编译器和处理器遵循两个重排序规则:

  1. 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
  2. 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

参考资料

  1. JSR-133: JavaTM Memory Model and Thread Specification
  2. The Java Memory Model

推荐阅读
  • 揭秘腾讯云CynosDB计算层设计优化背后的不为人知的故事与技术细节
    揭秘腾讯云CynosDB计算层设计优化背后的不为人知的故事与技术细节 ... [详细]
  • 如果程序使用Go语言编写并涉及单向或双向TLS认证,可能会遭受CPU拒绝服务攻击(DoS)。本文深入分析了CVE-2018-16875漏洞,探讨其成因、影响及防范措施,为开发者提供全面的安全指导。 ... [详细]
  • 深入解析Spring Boot启动过程中Netty异步架构的工作原理与应用
    深入解析Spring Boot启动过程中Netty异步架构的工作原理与应用 ... [详细]
  • 2021年7月22日上午9点至中午12点,我专注于Java的学习,重点补充了之前在视频中遗漏的多线程知识。首先,我了解了进程的概念,即程序在内存中运行时形成的一个独立执行单元。其次,学习了线程作为进程的组成部分,是进程中可并发执行的最小单位,负责处理具体的任务。此外,我还深入研究了Runnable接口的使用方法及其在多线程编程中的重要作用。 ... [详细]
  • Java集合框架特性详解与开发实践笔记
    Java集合框架特性详解与开发实践笔记 ... [详细]
  • 在Python网络编程中,多线程技术的应用与优化是提升系统性能的关键。线程作为操作系统调度的基本单位,其主要功能是在进程内共享内存空间和资源,实现并行处理任务。当一个进程启动时,操作系统会为其分配内存空间,加载必要的资源和数据,并调度CPU进行执行。每个进程都拥有独立的地址空间,而线程则在此基础上进一步细化了任务的并行处理能力。通过合理设计和优化多线程程序,可以显著提高网络应用的响应速度和处理效率。 ... [详细]
  • 技术分享:深入解析GestureDetector手势识别机制
    技术分享:深入解析GestureDetector手势识别机制 ... [详细]
  • JDK 1.8引入了多项并发新特性,显著提升了编程效率。本文重点探讨了LongAdder和StampedLock的特性和应用场景。此外,还介绍了在多线程环境中发生死锁时,如何通过jps命令进行诊断和排查,提供了详细的步骤和示例。这些改进不仅增强了系统的性能,还简化了开发者的调试工作。 ... [详细]
  • 近年来,BPM(业务流程管理)系统在国内市场逐渐普及,多家厂商在这一领域崭露头角。本文将对当前主要的BPM厂商进行概述,并分析其各自的优势。目前,市场上较为成熟的BPM产品主要分为两类:一类是综合型厂商,如IBM和SAP,这些企业在整体解决方案方面具有明显优势;另一类则是专注于BPM领域的专业厂商,它们在特定行业或应用场景中表现出色。通过对比分析,本文旨在为企业选择合适的BPM系统提供参考。 ... [详细]
  • 在并发编程中,`as-if-serial`原则确保了即使编译器和处理器对指令进行重排序,单线程的执行结果也不会受到影响。这一原则要求编译器、运行时环境和处理器必须严格遵守,以保证程序的正确性。本文深入探讨了`volatile`关键字的内存模型,详细分析了其在多线程环境中的可见性和有序性特性,以及如何通过`as-if-serial`规则来确保数据的一致性和可靠性。 ... [详细]
  • TypeScript 实战分享:Google 工程师深度解析 TypeScript 开发经验与心得
    TypeScript 实战分享:Google 工程师深度解析 TypeScript 开发经验与心得 ... [详细]
  • 为了向用户提供虚拟应用程序,通常会在基础架构中部署StoreFront或Web Interface。为了确保安全的远程访问,通常需要在DMZ中配置Secure Gateway或Access Gateway。本文详细对比了这两种界面工具的功能特性,包括用户管理、安全性、性能优化等方面,为企业选择合适的解决方案提供了全面的参考。 ... [详细]
  • 在基于.NET框架的分层架构实践中,为了实现各层之间的松散耦合,本文详细探讨了依赖注入(DI)和控制反转(IoC)容器的设计与实现。通过合理的依赖管理和对象创建,确保了各层之间的单向调用关系,从而提高了系统的可维护性和扩展性。此外,文章还介绍了几种常见的IoC容器实现方式及其应用场景,为开发者提供了实用的参考。 ... [详细]
  • 深入解析 ELF 文件格式与静态链接技术
    本文详细探讨了ELF文件格式及其在静态链接过程中的应用。在C/C++代码转化为可执行文件的过程中,需经过预处理、编译、汇编和链接等关键步骤。最终生成的可执行文件不仅包含系统可识别的机器码,还遵循了严格的文件结构规范,以确保其在操作系统中的正确加载和执行。 ... [详细]
  • 在Python编程中,探讨了并发与并行的概念及其区别。并发指的是系统同时处理多个任务的能力,而并行则指在同一时间点上并行执行多个任务。文章详细解析了阻塞与非阻塞操作、同步与异步编程模型,以及IO多路复用技术的应用。通过模拟socket发送HTTP请求的过程,展示了如何创建连接、发送数据和接收响应,并强调了默认情况下socket的阻塞特性。此外,还介绍了如何利用这些技术优化网络通信性能和提高程序效率。 ... [详细]
author-avatar
书友49812911
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有