热门标签 | 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

推荐阅读
  • 使用Boost.Asio进行异步数据处理的应用程序主要依赖于两个核心概念:I/O服务和I/O对象。I/O服务抽象了操作系统接口,使得异步操作能够高效地执行。I/O对象则代表了具体的网络资源,如套接字和文件描述符,通过这些对象可以实现数据的读写操作。本文详细介绍了这两个概念在Boost.Asio中的应用及其在网络编程中的重要性。 ... [详细]
  • 深入理解Spark框架:RDD核心概念与操作详解
    RDD是Spark框架的核心计算模型,全称为弹性分布式数据集(Resilient Distributed Dataset)。本文详细解析了RDD的基本概念、特性及其在Spark中的关键操作,包括创建、转换和行动操作等,帮助读者深入理解Spark的工作原理和优化策略。通过具体示例和代码片段,进一步阐述了如何高效利用RDD进行大数据处理。 ... [详细]
  • IIS 7及7.5版本中应用程序池的最佳配置策略与实践
    在IIS 7及7.5版本中,优化应用程序池的配置是提升Web站点性能的关键步骤。具体操作包括:首先定位到目标Web站点的应用程序池,然后通过“应用程序池”菜单找到对应的池,右键选择“高级设置”。在一般优化方案中,建议调整以下几个关键参数:1. **基本设置**: - **队列长度**:默认值为1000,可根据实际需求调整队列长度,以提高处理请求的能力。此外,还可以进一步优化其他参数,如处理器使用限制、回收策略等,以确保应用程序池的高效运行。这些优化措施有助于提升系统的稳定性和响应速度。 ... [详细]
  • 在iOS平台上,应用的流畅操作体验一直备受赞誉。然而,过去开发者往往将更多精力集中在功能实现上,而对性能优化的关注相对较少。本文深入探讨了iOS应用性能优化的关键要点与实践方法,旨在帮助开发者提升应用的响应速度、降低功耗,并改善整体用户体验。通过具体案例分析和技术解析,文章提供了实用的优化策略,包括代码层面的改进、资源管理优化以及界面渲染效率的提升等。 ... [详细]
  • 在第七天的深度学习课程中,我们将重点探讨DGL框架的高级应用,特别是在官方文档指导下进行数据集的下载与预处理。通过详细的步骤说明和实用技巧,帮助读者高效地构建和优化图神经网络的数据管道。此外,我们还将介绍如何利用DGL提供的模块化工具,实现数据的快速加载和预处理,以提升模型训练的效率和准确性。 ... [详细]
  • 深入解析Java虚拟机内存模型(JMM)及其核心机制
    为了深入理解Java内存模型(JMM),首先需要对计算机硬件体系有全面的认识,尤其是CPU与主存之间的多级缓存架构。这些硬件特性直接影响了JMM的设计和实现,确保在多线程环境下数据的一致性和可见性。 ... [详细]
  • JVM参数设置与命令行工具详解
    JVM参数配置与命令行工具的深入解析旨在优化系统性能,通过合理设置JVM参数,确保在高吞吐量的前提下,有效减少垃圾回收(GC)的频率,进而降低系统停顿时间,提升服务的稳定性和响应速度。此外,本文还将详细介绍常用的JVM命令行工具,帮助开发者更好地监控和调优JVM运行状态。 ... [详细]
  • 深入解析Wget CVE-2016-4971漏洞的利用方法与安全防范措施
    ### 摘要Wget 是一个广泛使用的命令行工具,用于从 Web 服务器下载文件。CVE-2016-4971 漏洞涉及 Wget 在处理特定 HTTP 响应头时的缺陷,可能导致远程代码执行。本文详细分析了该漏洞的成因、利用方法以及相应的安全防范措施,包括更新 Wget 版本、配置防火墙规则和使用安全的 HTTP 头。通过这些措施,可以有效防止潜在的安全威胁。 ... [详细]
  • NoSQL数据库,即非关系型数据库,有时也被称作Not Only SQL,是一种区别于传统关系型数据库的管理系统。这类数据库设计用于处理大规模、高并发的数据存储与查询需求,特别适用于需要快速读写大量非结构化或半结构化数据的应用场景。NoSQL数据库通过牺牲部分一致性来换取更高的可扩展性和性能,支持分布式部署,能够有效应对互联网时代的海量数据挑战。 ... [详细]
  • 在AXI4接口的读操作中,主设备通过读地址通道向从设备发送读请求,指定所需数据的地址及相关控制信号。随后,从设备通过读数据通道将指定地址上的数据传输回主设备。这一过程确保了高效的数据读取和传输,适用于多种高性能系统设计。 ... [详细]
  • 深入解析:使用C++实现Python字节数组(struct)的高效处理方法 ... [详细]
  • 深入解析 iOS Objective-C 中的对象内存对齐规则及其优化策略
    深入解析 iOS Objective-C 中的对象内存对齐规则及其优化策略 ... [详细]
  • 西北工业大学作为陕西省三所985和211高校之一,虽然在农业和林业领域不如某些顶尖院校,但在航空航天领域的实力尤为突出。该校的计算机科学专业在科研和教学方面也具有显著优势,是考研的理想选择。 ... [详细]
  • Go语言中Goroutine与通道机制及其异常处理深入解析
    在Go语言中,Goroutine可视为一种轻量级的并发执行单元,其资源消耗远低于传统线程,初始栈大小仅为2KB,而普通线程则通常需要几MB。此外,Goroutine的调度由Go运行时自动管理,能够高效地支持成千上万个并发任务。本文深入探讨了Goroutine的工作原理及其与通道(channel)的配合使用,特别是在异常处理方面的最佳实践,为开发者提供了一套完整的解决方案,以确保程序的稳定性和可靠性。 ... [详细]
  • 利用Redis HyperLogLog高效统计微博日活跃和月活跃用户数
    本文探讨了如何利用Redis的HyperLogLog数据结构高效地统计微博平台的日活跃用户(DAU)和月活跃用户(MAU)数量。通过HyperLogLog的高精度和低内存消耗特性,可以实现对大规模用户数据的实时统计与分析,为平台运营提供有力的数据支持。 ... [详细]
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社区 版权所有