JMM—Java内存模型 及其原子性、可见性、有序性

本文详细解析Java内存模型(JMM),阐述其与JVM的关系,解释主内存与工作内存概念,探讨JMM同步操作及三大特性:原子性、可见性和有序性,确保多线程环境下数据的一致性和安全性。

1.引入

众所周知,现在计算机多数还都是基于冯诺依曼计算机模型,这也是现代计算机物理结构的基础。Java应用拥有跨平台的特性,主要归功于Java虚拟机,它为Java应用提供了逻辑上的运行环境。如果说冯诺依曼计算机模型是物理结构上的划分,那么JVM就是逻辑结构上划分,那JMM又是什么呢?

2.概念

JMM((Java Memory Model)是一种抽象的概念,并不真实存在。它所定义的规范规定了程序中的变量在共享数据区域和私有数据区域的访问方式,主要是围绕原子性,有序性、可见性展开。
JMM把内存分为了主内存和工作内存。
主内存主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的局部变量,当然也包括了共享的类信息、常量、静态变量。
工作内存主要存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括了字节码行号指示器、相关Native方法的信息。
画个图简单了解一下:
在这里插入图片描述

3.JMM与JVM的关系

实际上,JMM与JVM并不存在必然的联系,因为它们是两种不同的概念。但如果一定要给它们做一些关联,那么JMM中的主内存(共享数据区域),包括了JVM的堆和方法区,JMM中的工作内存(私有数据区域),包括了程序计数器、虚拟机栈以及本地方法栈。
从上述的概念还可以看出,主内存在计算机硬件结构中就是指内存(内存条),工作内存指的是CPU中的缓存和寄存器。

4.JMM存在的意义

在JVM中,运行程序的实体是线程,线程所操作的数据也都是自己工作内存中的数据,这些数据只是主内存中数据(共享数据)的副本,如果两个线程同时对主内存中的同一数据进行操作,那么很容易发生线程的安全问题。
以“a=a+1”为例,线程1和线程2同时在主内存中读取了数据,假设a的初始值为0,那么都进行加1操作后,各自工作内存中的a值都变成了1,然后双方在将a的新值写入主内存,则a的值经过两个线程的加1操作后,最终变成1,这显然是不对的。
那么,这种问题怎么解决?JMM就是这种问题的解决方案,它定义了主内存与工作内存之间的具体交互协议。

5.JMM同步的8种操作

释义

1.lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态;
2.unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
3.read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中, 以便随后的load动作使用;
4.load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作 内存的变量副本中;
5.use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎;
6.assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量;
7.store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作;
8.write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送 到主内存的变量中。
来个图更清晰一些:
在这里插入图片描述
注意点:以上8个操作不一定是连续的,有可能数据load到工作内存中,而不马上use。虽然不一定连续,但一定是顺序的,肯定不存在先load的后read的情况。另外,read和load、store和write一定是成对并连续出现的。

同步规则

1.不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
2.一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load 或者assign)的变量。也就是对一个变量实施use和store操作之前,必须先自行assign和load 操作。
3.一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一线程重复 执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和 unlock必须成对出现。
4.如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新执行load或assign操作初始化变量的值。
5.如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
6.对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

6.JMM的3大特性

原子性

原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。比如a=0,作为一个写操作就是原子操作。值得注意的是,在64位操作系统中,Java的8中基本数据类型的读写操作是原子操作,但是在32位系统中long类型和double类型的读写操作却不是原子操作,因为它们占据的内存是64位。
我们知道,在平时的业务场景中,需要保证原子性的操作可不只是基本类型数据的读写操作,那么JMM中是如何保证其它操作的原子性呢?那就是synchronized关键字和Lock接口,因为synchronized和Lock能够保证任一时刻只有一个线程访问该代码块。

可见性

可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。还是以“a=a+1"为例:线程1将主内存中的a值读取到工作内存中,然后在工作内存中进行了加1操作;在线程1将其工作内存中的a值刷回主内存之前,线程2将主内存中a的旧值读取到了工作内存中进行加1操作;最终线程1和2分别找合适的时机将自己工作内存中的数据刷回主内存。我们可以看到,在这个操作中,明明a的值应该增加了2,而结果是只增加了1,这就是线程2对线程1中a值的不可见导致的。
在JMM中,是怎么保证数据的可见性的呢?最简单的一个就是volatile关键字,它能保证一个共享变量在某一线程中被改变后立刻刷回主内存,其它线程读到的数只能是主内存中的新值。另外,synchronized和Lock也可以保证可见性,因为它们可以保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到内存中。

有序性

在讲有序性之前,不得不说另外一个名词——指令重排序。为了提高程序的执行效率,在不影响程序的运行结果的前提下(仅针对单线程),指令的执行顺序可以被改变,这就是指令重排序。指令重排必须遵守as-if-serial语义,即:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。指令重排主要发生在编译阶段和运行阶段。
有序性主要就是在多线程的场景下,保证不进行指令重排,使指令的执行顺序与程序中的代码的顺序一致。
下面说一个指令重排(无序)的例子,让我们看一下有序性的重要性:主内存中有两个共享变量“a=0,b=0”;线程1将执行操作“x=a,b=1”;线程2将执行操作“y=b,a=1”;线程1和2同时启动,并多试几次,你会发现有多种(x,y)值的组合,如(0,0)、(0,1)、(1,0)、(1,1)。
导致这些结果的原因之一就是由于指令重排,对于单线程来讲,线程1中的x=a和b=1可以交换顺序,线程2中的y=b和a=1也可以交换顺序,这就导致了结果的不确定性,然而在很多时候这种事情是不允许发生的,也就是需要保证有序性。
在JMM中,可以通过volatile关键字来保证一定的“有序性”,因为它提供了内存屏障防止指令重排。当然synchronized和Lock也可以保证有序性,因为它们另代码变成了同步代码块,自然不会有指令乱序问题。

7.happens-before 原则

我们知道,线程修改变量值,直接修改的是工作内存中的值,而不是主内存中的变量值,那么修改后的值什么时候会刷新到主内存呢?JMM中除了使用sychronized和volatile保证3大特性还有没有别的办法呢?
从JDK 1.5开始,Java使用新的JSR-133内存模型,提供了happens-before原则来解决上述问题,那么happens-before原则的内容有哪些呢?
1.程序顺序规则。在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
2.管理锁定规则。解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
3.volatile变量规则。volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单 的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的新值。
4.线程启动规则。线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见。
5.线程终止规则。 线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
6.线程中断规则。对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中 断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
7.对象终结规则。 对象的构造函数执行,结束先于finalize()方法。
8.传递性。A先于B ,B先于C,那么A必然先于C。

【个人笔记】转载请注明出处!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值