java内存模型总结

简介: 在说java内存模型之前,先澄清下JVM内存结构与Java内存模型JVM内存结构和Java虚拟机的运行时区域有关Java内存模型和Java的并发编程有关JVM内存结构,我们都知道java是要运行在虚拟机上的,而虚拟机在执行Java程序的过程中会把所管理的内存划分为若干不同的数据区域,这些区域都有各自的用途。在《Java虚拟机规范jdk8》中说了JVM运行是内存区域结构可以分为6个区。堆区虚拟机栈方法区本地方法栈程序计数器运行时常量池以上是java虚拟机规范,不同的虚拟机实现会各有不同,一般会准守规范这里总结下,JVM内存结构由java虚拟机规范定义的

在说java内存模型之前,先澄清下JVM内存结构与Java内存模型

JVM内存结构和Java虚拟机的运行时区域有关

Java内存模型和Java的并发编程有关

JVM内存结构,我们都知道java是要运行在虚拟机上的,而虚拟机在执行Java程序的过程中会把所管理的内存划分为若干不同的数据区域,这些区域都有各自的用途。在《Java虚拟机规范jdk8》中说了JVM运行是内存区域结构可以分为6个区。

堆区

虚拟机栈

方法区

本地方法栈

程序计数器

运行时常量池

以上是java虚拟机规范,不同的虚拟机实现会各有不同,一般会准守规范

这里总结下,JVM内存结构由java虚拟机规范定义的,描述的是java程序执行过程中,由JVM管理的不同数据区域,各个区域有其特定的功能。

以上是JVM内存结构,现在说下Java内存模型上,我们都知道,编写的Java代码,最终还是转化成CPU指令才能执行。我们先回顾下从Java代码到最终执行的CPU指令的大致流程:

最开始,我们编写的Java代码,是*.java文件

在编译(包含词法分析、语义分析等步骤)后,在刚才的.java文件之外,会多出一个新的Java字节码文件,(.class)

JVM会分析刚才生成的字节码文件(*.class),并根据平台等因素,把字节码文件转化为具体平台上的机器指令。

机器指令则可以直接在CPU上运行,也就是最终的程序执行。

那么为啥会有JMM,在早期语言中,其实不存在内存模型的概念。

所以程序最终执行的效果会依赖具体的处理器,而不同的处理器的规则又不一样,不同的处理器之间的差异很大,因此同样一段代码,可能在处理器A上运行正常,而在处理器B上运行的结果却不一致。同理在没有JMM之前,不同的JVM实现,也会带来不同的翻译结果。

所以Java非常需要一个标准,来让开发者、编译器工程师和JVM工程师能够达成一致,达成一致后,我们就可以很清楚的知道什么的代码最终可以达到什么样的效果,让多线程运行结果可以预期,这个标准就是JMM,这也就是JMM出现的原因。

下面我们要研究从Java代码到CPU 指令的这个转化过程要遵循哪些和并发相关的原则和规范,这就是JMM的重点内容。如果不加以规范,那么同样的Java代码,完全可能产生不一样的执行效果,那是不可接受的。

JMM是什么

JMM是一个规范,是和多线程相关的一组规范,是需要各个JVM的实现来遵守的JMM规范,以便开发者可以利用这些规范,更方便的开发多线程程序。这样一来即便同一个程序在不同的虚拟机上运行,得到的程序结果也是一致的。

如果没有JMM内存模型规范,那么很可能在经过不同JVM翻译之后,导致在不同的虚拟机上运行的结果不一样。

因此,JMM与处理器、缓存、并发、编译器有关,它解决了CPU多级缓存、处理器优化、指令重排序等导致的结果不可预期的问题。

JMM工具类和关键字的原理,比如volatile、synchronized、Lock等,其实他们都涉及到JMM,关键字synchronized,JVM就会在JMM规则下,“翻译”出合适的指令,包括限制指令之间的顺序,以便在即使发生重排序的情况下,也能保证必要的“可见性”,这样一来,不同的JVM对于相同的代码的执行结果就变的可预期了,我们只需要用好同步工具和关键字就可以开发出正确的并发程序了。

JMM里最重要的3点,重排序、原子性、内存可见性。

重排序,假设写一个Java程序,包含一系列的语句,我们会默认期望这些语句的实际运行顺序和写的代码顺序一致,但是实际上,编译器、JVM或者CPU都有可能出于优化等目的,对于实际指令执行的顺序进行调整,这就是重排序。

重排序的好处:提高处理速度。

举个例子,重排序之前

              部分指令执行情况

a=100; Load a Set to 100 Store a

b=5; Load b Set to 5 Store b

a=a+10; Load a Set to 110 Store a

从这个上面我们可以看出来,会有两次Load a 和两次Store a,存在一定的重排序的优化空间

重排序后,

a=100; Load a Set to 100 Set to 110 Store a

a=a+10 ;

b=5 ; Load b Set to 5 Store b

重排序一般有3中情况

(1)编译器优化

包括JVM JIT编辑器等,重排序并不意味着可以任意排序,它需要保证重排序后,不改变单线程内的语义,否则如果能任意排序的话,程序就乱了

(2)CPU重排序

CPU同样会有优化行为,这里的优化和编译器优化类似,都是通过乱序执行的技术来提高整体的执行效率。所以即使之前编译器不发生重排序,CPU也可能进行重排。

(3)内存的“重排序”

内存系统不存在真正的重排序,但是内存会带来看上去和重排序一样的效果 ,所以重排序打上引号,由于内存有缓存的存在,在JMM里表现为主存和本地内存,而主存和本地内存的内容可能不一致,所以会导致程序表现出乱序的行为。

原子性和原子操作

什么是原子性和原子操作

原子性操作,具有原子性的操作是原子性操作,比如转账操作,转账这边的人要进行扣款,银行会有转账记录,收到转
账的人账号金额要增加,这一系列操作不能中间中断,再举一个不具有原子性操作的例子,i++,初始i的值,然后i+1
,最后在保存这个值,当两个线程,都来操作这个变量,当线程1,操作到i+1时,CPU终止这个线程1,CPU又用线程2,
来操作这个变量i,这个时候,取到的i的值还是1,因为线程1操作i的值还没有保存下来,没有执行完成。线程2这个时
候继续执行i+1,这个时候i变为2。这个时候CPU又把线程2停止了,转到线程1上继续之前的操作,保存i的值,保存成2
。然后CPU暂停线程1,转到线程2,线程2继续执行,保存下来的也是2,而不是3。这个就是典型的线程安全问题。在
java中有8中基本数据类型,其中除了long类型和double类型,其余6中基本数据类型都自带原子性,来保证线程安全性
,但是long和double两个数据类型,没有自带原子性,建议加volatile关键字修饰,但是我们再编码的时候使用long和
double的时候也没有加volatile,是JVM虚拟机强烈建议,把该操作,作为原子操作来实现。

保证内存可见性,volatile只能保证内存可见性,不能保证原子性。synchronized既能保证线程安全,应该说它既保证
内存可见性,又能保证原子性,内存可见性在前一个线程释放锁之后,之前所做的所有修改,都能被获得同一个锁的下
一个线程所看到,也就是能读取到最新的值。原子性,临界区内,最多同时只允许一个线程执行操作。

为什么会发生可见性的问题
CPU是有多级缓存构成,导致读的数据存在过期性。由于CPU处理的速度很快,内存的速度就显得很慢,所以就出现CPU
多级缓存的存在,就是在CPU和内存之间存在 一个缓存层,缓存层有三层,从下往上一次是L1、L2、L3,越往上速度越
快,容量也越小。因为出现多级缓存,肯定会导致数据读取的不一致行。
Java语言,屏蔽的CPU的L1 L2 L3,这些多级缓存的底层作用,我们只需要

主内存和工作内存
Java语言,屏蔽了CPU 的多级缓存L1 L2 L3等这些底层的缓存细节,我们只需要关心JMM抽象出来的主内存和工作内存
,如图,每个线程只能操作各自的工作内存,而不能操作主内存,工作内存里保存的是主内存中的副本,主内存和工作
内存之间通信的是靠JMM控制。

JMM有以下规定:

所有变量都保存在主内存中,同时每个线程拥有自己独立的工作内存,
工作内存的变量是从主内存中拷贝出来的

线程不能直接操作主内存的数据,只能操作自己工作内存的变量,然后再同步
到主内存中,这样其他线程就可以看到本次修改

主内存是由多个线程所共享的,但是线程间不共享各自的工作内存,每
个线程必须操作各自的工作线程,然后同步到主内存,再共享给其他线程

happens-before
happens-before规则是用来描述和可见性相关的问题的:
如果第一个操作happens-before第二个操作
那么第一个操作对于第二个操作一定是可见的
也就是第二个操作在执行时就一定能保证看见第一个操作执行的结果
不具备happens-before规则的例子
happens-before规则有哪些?
如果分别操作x和操作y,用hb(x,y)来表示x happens-beforey
(1)单线程规则:
在一个单独的线程中,按照程序代码的执行流顺序
(时间上的先后顺序,下同)先执行的操作happens-before后执行的操作
也就是,如果操作x和操作y是同一个线程的两个操作
并且在代码执行上x先于y出现,那么有hb(x,y)
Happens-beford规则和重排序冲突,如果happens-before,就不能重排序吗?
答案是否定的,例如,单线程内,语句1在语句2的前面
根据“单线程规则”,语句1happens-before语句2
例如语句1修改的是变量a的值,而语句2的内容和变量a无关
那么语句1和语句2依然有可能被重排序
如果语句1修改的是变量2,而语句2正好是去读取变量a的值
那么语句1就一定会在语句2之前执行。
(2)锁操作的规则(synchronized和Lock接口等) 重点
如果操作A是解锁,而操作B是对同一个锁的加锁,那么hb(A,B),在解锁M之前的所有操作,对于加锁M之后的所有操
作都是可见的。
(3)volatile变量规则, 重点
对一个volatile变量的写操作happens-before后面对该变量的读操作
这就代表了如果变量被volatile修饰,那么每次修改之后其他线程在读取这个变量的时候一定能读取到该变量最新的值
volatile关键字能保证可见性。正式有本条规则所规定的
(4)线程启动规则
Thread对象的start方法happens-before此线程run方法中的每一个操作
线程启动规则
子线程B 在执行run()方法里面的语句的时候,一定会看到,thread.start()之前的所有结果
(5)线程join规则
join可以让线程之间等待,
在join方法返回后,线程B的run方法里面的操作happens-before线程A的join之后的语句,
就是线程A中 它threadB.join()后面的方法,都可以看见线程中的run()里的内容
(6)中断规则
对线程interrupt方法的调用happens-before检测该线程的中断事件
如果一个线程被其他线程interrupt
那么在检测中断时(比如调用Thread.interrupt或者Thread.isInterrupt方法)时,一定能看到此次中断的发生,不会
发生检测结果不准的情况。
(7)工具类的Happens-before规则
线程安全的并发容器(如ConcurrentHashMap)
在get某个值时一定能看到在此之前发生的put等存入操作的结果
线程安全的并发容器的存入操作happens-before读取操作
信号量(Semaphore)会释放许可证,也会获取许可证
这里的释放许可证的操作happens-before获取许可证的操作
如果在获取许可证之前有释放许可证的操作,那么在获取时一定可以看到
Future:Future任务中的所有操作happens-before Future的get操作
线程池:要想利用线程池,就需要往里面提交任务(Runnable或者Callable)
这里面也有一个Happens-Before原则,那就是提交任务的操作happens-before任务的执行

volatile 和synchronized区别
volatile是一种同步机制
当某个变量是共享变量,且这个变量是被volatile修饰的,那么在修饰了这个变量的值之后再读取该变量的值时,可以
保证获取到的值是修改后的最新的值。
相比synchronized或者Lock,volatile是更轻量的
使用volatile不会发生上下文切换等开销很大的情况,不会让线程阻塞,但是它的效果,也是能力,相对也小。
volatile仅在有限的场景中才能发挥作用。
不适合使用volatile场景,不适合需要保证原子性的场景,比如a++,更新的时候需要原来的值,使用volatile是不能
保证线程安全的。
适用场景:1、布尔标记位,如果某个共享变量自始至终只是被各个线程所赋值或读取,而没有其他的操作,那么就可
以使用volatile来代替synchronized或者代替原子类。因为赋值操作自身具有原子性的,volatile同时又保证可见性。
一个典型的场景就是布尔标记位的场景,例如volatile boolean flag,
通常情况下,boolean类型的标记位会被直接赋值
此时不会存在符合操作(如a++),只存在单一操作
flag被volatile修饰之后,就保证了可见性
那么这个flag就可以当作一个标记位
此时它的值一单发生了变化,所有线程都可以立刻看到
2、作为触发器

volatile的作用:
第一层的作用是保证可见性
第二层的作用是禁止指令重排序
volatile和synchronized的关系
相似性
volatile可以看做是一个轻量级的synchronized
比如一个共享变量如果自始至终只被各个线程赋值和读取,而没有其他操作的话
就可以用volatile来代替synchronized或者代替原子变量,足以保证线程安全
对volatile字段的每次读取或者写入都类似于“半同步”
读取volatile与获取synchronized锁有相同的内存语义
写入volatile与释放synchronized锁有相同的内存语义
不可代替
volatile是不能代替synchronized的,volatile并没有提供原子性和互斥性
性能方面
volatile属性的读写操作都是无锁的,是高性能的,比synchronized性能更好。

单例模式,双重校验,并且用volatile修饰变量,作用是禁止指令重排序。

目录
相关文章
|
17天前
|
存储 Java 编译器
Java内存模型(JMM)深度解析####
本文深入探讨了Java内存模型(JMM)的工作原理,旨在帮助开发者理解多线程环境下并发编程的挑战与解决方案。通过剖析JVM如何管理线程间的数据可见性、原子性和有序性问题,本文将揭示synchronized关键字背后的机制,并介绍volatile关键字和final关键字在保证变量同步与不可变性方面的作用。同时,文章还将讨论现代Java并发工具类如java.util.concurrent包中的核心组件,以及它们如何简化高效并发程序的设计。无论你是初学者还是有经验的开发者,本文都将为你提供宝贵的见解,助你在Java并发编程领域更进一步。 ####
|
28天前
|
缓存 easyexcel Java
Java EasyExcel 导出报内存溢出如何解决
大家好,我是V哥。使用EasyExcel进行大数据量导出时容易导致内存溢出,特别是在导出百万级别的数据时。以下是V哥整理的解决该问题的一些常见方法,包括分批写入、设置合适的JVM内存、减少数据对象的复杂性、关闭自动列宽设置、使用Stream导出以及选择合适的数据导出工具。此外,还介绍了使用Apache POI的SXSSFWorkbook实现百万级别数据量的导出案例,帮助大家更好地应对大数据导出的挑战。欢迎一起讨论!
148 1
|
12天前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
35 6
|
16天前
|
存储 缓存 安全
Java内存模型(JMM):深入理解并发编程的基石####
【10月更文挑战第29天】 本文作为一篇技术性文章,旨在深入探讨Java内存模型(JMM)的核心概念、工作原理及其在并发编程中的应用。我们将从JMM的基本定义出发,逐步剖析其如何通过happens-before原则、volatile关键字、synchronized关键字等机制,解决多线程环境下的数据可见性、原子性和有序性问题。不同于常规摘要的简述方式,本摘要将直接概述文章的核心内容,为读者提供一个清晰的学习路径。 ####
35 2
|
17天前
|
存储 安全 Java
什么是 Java 的内存模型?
Java内存模型(Java Memory Model, JMM)是Java虚拟机(JVM)规范的一部分,它定义了一套规则,用于指导Java程序中变量的访问和内存交互方式。
42 1
|
23天前
|
存储 运维 Java
💻Java零基础:深入了解Java内存机制
【10月更文挑战第18天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
28 1
|
26天前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
1月前
|
存储 监控 算法
Java中的内存管理与垃圾回收机制解析
本文深入探讨了Java编程语言中的内存管理方式,特别是垃圾回收机制。我们将了解Java的自动内存管理是如何工作的,它如何帮助开发者避免常见的内存泄漏问题。通过分析不同垃圾回收算法(如标记-清除、复制和标记-整理)以及JVM如何选择合适的垃圾回收策略,本文旨在帮助Java开发者更好地理解和优化应用程序的性能。
|
1月前
|
存储 Java
Java内存模型
【10月更文挑战第11天】Java 内存模型(JMM)是 Java 虚拟机规范中定义的多线程内存访问机制,解决内存可见性、原子性和有序性问题。它定义了主内存和工作内存的概念,以及可见性、原子性和有序性的规则,确保多线程环境下的数据一致性和操作正确性。使用 `synchronized` 和 `volatile` 等同步机制可有效避免数据竞争和不一致问题。
29 3
|
1月前
|
缓存 安全 Java
使用 Java 内存模型解决多线程中的数据竞争问题
【10月更文挑战第11天】在 Java 多线程编程中,数据竞争是一个常见问题。通过使用 `synchronized` 关键字、`volatile` 关键字、原子类、显式锁、避免共享可变数据、合理设计数据结构、遵循线程安全原则和使用线程池等方法,可以有效解决数据竞争问题,确保程序的正确性和稳定性。
38 2