并发编程(四)JMM内存模型
文章更新时间:2021/07/10
一、什么是JMM
概念:Java内存模型是一种抽象的概念,并不真实存在,定义了Java程序在各种平台下对内存访问的机制及规范。
PS:线程是程序运行的载体
图示:
解析:首先我们要明确一点:内存模型主要是影响线程共享的内存可见性问题,Java线程之间的通信由Java内存模型【JMM】控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:
- 线程之间的共享变量存储在主内存(main memory)中
- 每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。
PS:本地内存是JMM的一个抽象概念,并不真实存在,它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
主内存
主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中。
PS:由于是共享数据区域,多条线程对同一个变量进行访问可能会发生线程安全问题。
工作内存
主要存储当前方法的所有本地变量信息【工作内存中存储着主内存中的变量副本拷贝】。
每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括了字节码行号指示器、相关Native方法的信息。
PS:由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。
PS:在主内存中的实例对象可以被多线程共享,倘若两个线程同时调用了同一个对象的同一个方法,那么两条线程会将要操作的数据拷贝一份到自己的工作内存中,执行完成操作后才刷新到主内存。
二、JMM在线程通信中起到的作用
线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
- 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
- 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。
下面通过示意图来说明这两个步骤:
解析:如上图所示,本地内存A和B有主内存中共享变量x的副本,那么在实现通信时,JMM内存模型是这样展现的:
- 假设初始时,这三个内存中的x值都为0。
- 线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。
- 当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。
- 随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1,线程通信完成
三、JMM内存模型与JVM内存结构的区别
JMM【Java内存模型】:指的在java程序运行过程中,计算机有主内存,每个java线程有自己的工作内存。【主要针对多线程使用内存的一种抽象概念,看做一种规范】
JVM【Java内存结构】:讲的是Java虚拟机内存的结构划分,包括堆区,栈区,方法区等。【主要针对java内存的管理】
PS:JMM是抽象概念,并不真实存在。
PS:在概念理解时,尽量不要把JVM和JMM结合起来联想,这样很容易混淆,JMM是一种针对多线程的抽象概念和规范,我们应该先理解清楚了概念再整合实际内存进行理解。
四、数据同步的八大原子操作
- lock(锁定):把一个变量标记为一条线程独占状态
- unlock(解锁):把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read(读取):把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load(载入):把read操作从主内存中得到的变量值放入工作内存的变量副本中
- use(使用):把工作内存中的一个变量值传递给执行引擎
- assign(赋值):把一个从执行引擎接收到的值赋给工作内存的变量
- store(存储):把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
- write(写入):把store操作从工作内存中的一个变量的值传送到主内存的变量中
五、并发编程的可见性,原子性与有序性问题
原子性
定义:原子性指的是一个操作是不可中断的,不可分割的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
可见性
定义:可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。
有序性
定义:程序执行的顺序按照代码的先后顺序执行。
对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的,但对于多线程环境,则可能出现乱序现象【指令重排导致】,重排后的指令与原指令的顺序未必一致。
六、JMM如何解决原子性、可见性、有序性问题
原子性问题
除了JVM自身提供的对基本数据类型读写操作的原子性外,可以通过synchronized和Lock实现原子性。【synchronized和Lock能够保证任一时刻只有一个线程访问该代码块】
可见性问题
- volatile关键字可以保证可见性。
- 当一个共享变量被volatile修饰时,它会保证修改的值立即被其他的线程看到,即修改的值立即更新到主存中,当其他线程需要读取时,它会去内存中读取新值。
- synchronized和Lock也可以保证可见性。
- 因为它们可以保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到内存中。
有序性问题
可以通过synchronized和Lock来保证有序性。
synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
七、并发编程涉及的一些重要原则
as-if-serial语义
定义:不管怎么重排序【编译器和处理器为了提高并行度】,(单线程)程序的执行结果不能被改变。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序, 因为这种重排序会改变执行结果。
PS:如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
happens-before原则
1、程序顺序原则
在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
2、锁规则
解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,即若对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
3、volatile规则
volatile的可见性保证:变量的写,先发生于读。
简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻不同的线程总是能够看到该变量的最新值。
4、线程启动规则
线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start()方法之前修改了共享变量的值,那么当线程B执行start()方法时,线程A对共享变量的修改对线程B可见。
5、传递性
A先于B ,B先于C 那么A必然先于C。
6、线程终止规则
场景:主线程A执行时,B线程调用Thread.join()方法。
- 假设在线程B终止之前修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
7、线程中断规则
对线程interrupt()方法的调用要比代码检测中断事件先发生,可以通过Thread.interrupted()方法检测线程是否中断。
8、对象终结规则
对象的构造函数执行,结束先于finalize()【当垃圾回收器将要回收对象时执行】方法。PS:不推荐使用finalize()方法
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律