Java内存模型基础

简介: Java内存模型基础

简介:

Java线程之间的通信对程序员完全透明,内存可见性问题很容易困扰Java程序员,这一系列几篇文章将揭开Java内存模型的神秘面纱。这一系列的文章大致分4个部分,分别是:


Java内存模型基础,主要介绍内存模型相关基本概念

Java内存模型中的顺序一致性,主要介绍重排序与顺序一致性内存模型

同步原语,主要介绍三个同步原语(synchronized、volatile和final)的内存语义及重排序规则在处理器中的实现

Java内存模型的设计,主要介绍Java内存模型的设计原理,及其与处理器内存模型和顺序一致性内存模型的关系。

一、Java内存模型的基础

1.1 并发编程模型的两个关键问题

在并发编程中需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体)。

通信——线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。


共享内存:线程之间共享程序的公共状态,通过读写内存中的公共转台进行隐式通信

消息传递:线程之间没有公共状态,线程之间必须通过发送消息来显式进行通信

同步——程序中用于控制不同线程键操作发生相对顺序的机制。


共享内存:同步是显式进行的,由于程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行

消息传递:同步是隐式进行的,由于消息的发送必须在消息的接收之前。

总结:

Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明,如果编写多线程程序的Java程序员不理解隐式进行线程之间的通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。


1.2 Java内存模型的抽象结构

Java中所有的实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享(文章中用“共享变量”指代)。局部变量(Local Variables)、方法定义参数(Formal Method Parameters)和异常处理器参数(Exception Handler Parameters)不会在线程之间共享,它们不会存在内存可见性问题,因此也不受内存模型的影响。

Java线程之间的通信由Java内存模型(JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存时JMM的一个抽象概念,并不真实存在。JMM涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。

image.png从上图来看,线程A和线程B之间要通信的话,必须经历下面2个步骤。

  1. 线程A把本地内存A中更新过的变量刷新到主内存中
  2. 线程B到主内存中去读取线程A之前已更新过的共享变量
    线程之间通信示意图

image.png如上图所示,本地内存A和本地内存B有主内存中共享变量X的副本。假设初始时,这三个内存中的X的值都是0.线程A在执行时,把更新后的X的值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信是,线程A首先把自己本地内存中修改后的X刷新到主内存中,此时主内存中的X值变为了1.随后,线程B到主内存中去读取线程A更新后的X值,此时线程B的本地内存X的值也更新成了1。

从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证。


1.3 从源代码到指令重排序

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分为三种类型:


编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将对跳指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应及其指令的执行顺序。

内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从Java源代码的最终实际执行的指令序列,会分别经历下面3种重排序,其中1属于编译器重排序,2和3属于处理器重排序。


源代码到最终执行的指令序列示意图

image.png重排序可能会导致多线程程序出现内存可见性问题,对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都需要禁止)。对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barries, Intel称之为Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。

JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保障。


1.4 写缓冲区和内存屏障

1.4.1 写缓冲区

现代处理器都会使用写缓冲区临时保存向内存中写入的数据。写缓冲区的主要作用:


可以保证指令流水线持续运行,可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟。

它以批处理的方式方式刷新写缓冲区,以及合并写缓冲区中对统一地址的多次写,减少对内存总线的占用。

常见处理器允许的重排序类型(Y-表示允许两个操作重排序,N-表示处理器不允许两个操作重排序)

image.png说明:常见处理器都允许Store-Load重排序;常见的处理器都不允许对存在数据依赖性的操作做重排序。N多的表示处理器拥有相对较强的处理器内存模型。


由于写缓冲器仅仅只对它所在的处理器可见,这个特性会对内存操作的执行顺序产生非常重要的影响:处理器对内存的读/写操作的执行顺序,不一定与内存实际发生的读/写操作顺序一致。

举例说明:

image.png假设处理器A和处理器B按程序的顺序并行执行内存访问,最终可能得到x=y=0的结果,具体原因如下:image.png说明:处理器A和处理器B可以同时把共享变量写入自己的写缓冲区(A1、B1),然后从内存中读取另一个共享变量(A2、B2),最后才把自己写缓冲区中保存的脏数据刷新到内存中(A3、B3)。当以这种时序执行时,程序就可以得到x=y=0结果。


1.4.2 内存屏障

为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM把内存屏障指令分为4类:

image.pngStoreLoad Barriers是一个“全能型屏障”,它同时具有其它3个屏障的效果。现代大多数处理器支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为处理器需要把缓冲区的内容全部刷新到内存中(Buffer Fully Flush)。


1.5 happens-before 简介

从JDK1.5开始,Java使用新的JSR-133内存模型。JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。这里的两个操作可以是单线程也可以是多线程。

happens-before规则:


程序顺序规则:一个线程中的每个操作,happens-before于该线程的任意后续操作。

监视器锁规则:对于一个锁的解锁,happens-before于随后对这个锁的加锁。

volatile变量规则:对于一个volitale域的写,happens-before于任意后续对这个volatile域的读。

传递性:如果A happens-before B,且B happens-before C ,那么A happens-before C。


注意:

两个操作之间具有happens-before关系,并不意味着前一个操作必须在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visiable to and ordered beofre the second)。

image.pnghappens-before与JMM的关系

一个happens-before规则对应于一个或多个编译器个处理器重排序规则。对于Java程序员来说,happens-before规则简单易懂,它避免了Java程序员为了理解JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现方法。



目录
相关文章
|
3月前
|
安全 Java 应用服务中间件
Spring Boot + Java 21:内存减少 60%,启动速度提高 30% — 零代码
通过调整三个JVM和Spring Boot配置开关,无需重写代码即可显著优化Java应用性能:内存减少60%,启动速度提升30%。适用于所有在JVM上运行API的生产团队,低成本实现高效能。
296 3
|
4月前
|
存储 缓存 Java
Java数组全解析:一维、多维与内存模型
本文深入解析Java数组的内存布局与操作技巧,涵盖一维及多维数组的声明、初始化、内存模型,以及数组常见陷阱和性能优化。通过图文结合的方式帮助开发者彻底理解数组本质,并提供Arrays工具类的实用方法与面试高频问题解析,助你掌握数组核心知识,避免常见错误。
|
2月前
|
Java 大数据 Go
从混沌到秩序:Java共享内存模型如何通过显式约束驯服并发?
并发编程旨在混乱中建立秩序。本文对比Java共享内存模型与Golang消息传递模型,剖析显式同步与隐式因果的哲学差异,揭示happens-before等机制如何保障内存可见性与数据一致性,展现两大范式的深层分野。(238字)
70 4
|
2月前
|
存储 缓存 Java
【深入浅出】揭秘Java内存模型(JMM):并发编程的基石
本文深入解析Java内存模型(JMM),揭示synchronized与volatile的底层原理,剖析主内存与工作内存、可见性、有序性等核心概念,助你理解并发编程三大难题及Happens-Before、内存屏障等解决方案,掌握多线程编程基石。
|
3月前
|
缓存 监控 Kubernetes
Java虚拟机内存溢出(Java Heap Space)问题处理方案
综上所述, 解决Java Heap Space溢出需从多角度综合施策; 包括但不限于配置调整、代码审查与优化以及系统设计层面改进; 同样也不能忽视运行期监控与预警设置之重要性; 及早发现潜在风险点并采取相应补救手段至关重要.
530 17
|
7月前
|
存储 缓存 Java
【高薪程序员必看】万字长文拆解Java并发编程!(5):深入理解JMM:Java内存模型的三大特性与volatile底层原理
JMM,Java Memory Model,Java内存模型,定义了主内存,工作内存,确保Java在不同平台上的正确运行主内存Main Memory:所有线程共享的内存区域,所有的变量都存储在主存中工作内存Working Memory:每个线程拥有自己的工作内存,用于保存变量的副本.线程执行过程中先将主内存中的变量读到工作内存中,对变量进行操作之后再将变量写入主内存,jvm概念说明主内存所有线程共享的内存区域,存储原始变量(堆内存中的对象实例和静态变量)工作内存。
237 0
|
4月前
|
监控 Kubernetes Java
最新技术栈驱动的 Java 绿色计算与性能优化实操指南涵盖内存优化与能效提升实战技巧
本文介绍了基于Java 24+技术栈的绿色计算与性能优化实操指南。主要内容包括:1)JVM调优,如分代ZGC配置和结构化并发优化;2)代码级优化,包括向量API加速数据处理和零拷贝I/O;3)容器化环境优化,如K8s资源匹配和节能模式配置;4)监控分析工具使用。通过实践表明,这些优化能显著提升性能(响应时间降低40-60%)同时降低资源消耗(内存减少30-50%,CPU降低20-40%)和能耗(服务器功耗减少15-35%)。建议采用渐进式优化策略。
209 1
|
4月前
|
存储 监控 算法
Java垃圾回收机制(GC)与内存模型
本文主要讲述JVM的内存模型和基本调优机制。
|
5月前
|
SQL 缓存 安全
深度理解 Java 内存模型:从并发基石到实践应用
本文深入解析 Java 内存模型(JMM),涵盖其在并发编程中的核心作用与实践应用。内容包括 JMM 解决的可见性、原子性和有序性问题,线程与内存的交互机制,volatile、synchronized 和 happens-before 等关键机制的使用,以及在单例模式、线程通信等场景中的实战案例。同时,还介绍了常见并发 Bug 的排查与解决方案,帮助开发者写出高效、线程安全的 Java 程序。
240 0