Java内存模型
创始人
2024-03-19 05:24:42
0

@TOC

Java内存模型

Java内存模型(Java Memory Model,JMM) 是《Java虚拟机规范》中定义的一种用来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致性的内存访问效果的一种内存访问模型。从JDK5开始 JMM才正真成熟,完善起来。

Java内存模型的主要目的是定义程序中各种变量(Java中的实例字段,静态字段和构成数组中的元素,不包括线程私有的局部变量和方法参数)的访问规则

Java内存模型规定了所有的变量都存储在主内存(Main Memory)。每条线程都有自己的工作内存(Working Memory),用来保存被该线程使用的变量的主内存副本。线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。不同线程之间无法之间访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成

线程、主内存、工作内存三者的交互关系如下图。

在这里插入图片描述

内存交互

关于一个变量如何从工作内存拷贝到工作内存,如何从工作内存同步回主内存这一类的实现细节,Java内存模型中定义了以下8种操作来完成。Java虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可在分的(对 double 和 long 类型的变量来说,load、store、read 和 write操作在某些平台上允许有例外)。

  • lock(锁定):作用于主内存的变量,它把一个变量表示为一条线程独占的状态。
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其它线程锁定。
  • read(读取):作用域工作内存的变量,它把一个变量的值从工作内存传输到线程的工作内存中,以便随后的 load 使用。
  • load(载入):作用于工作内存中的变量,它把 read 操作从主内存中得到的变量值放到工作内存中的变量副本中。
  • use(使用):作用于工作内存中的变量,它把工作内存中的一个变量值传给执行引擎,每当虚拟机遇到一个需要使用变量值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存中的变量,它把一个从执行引擎接受的值赋给工作内存中的变量,每当虚拟机遇到一个需要使用变量赋值的字节码指令时执行此操作。
  • store(赋值):作用于工作内存中的变量,它把工作内存中的一个变量传递到主内存中,以 write 操作使用。
  • write(赋值):作用于主内存中的变量,他把 store 操作传递来的值放入到主内存的变量中。

它们的使用规则如下:

  • 不允许 read 和 load、store 和 write 操作之一单独出现。
  • 不允许一个线程丢弃它最近的assign操作,即一个变量在工作内存中被修改后必须把该变化同步回主内存。
  • 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从线程工作内存同步回主内存中。
  • 一个新的变量只能在主内存中 “诞生”,不允许在工作内存中直接使用一个未被初始化的(load 或 assign)的变量,即对一个变量实施 use 或 store 操作之前,必须先执行 load 或 assign操作。
  • 一个变量在同时个时刻治愈系一条线程对器进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次 lock 后,必须执行相同次数的 unlock 操作,变量才会被解锁。
  • 如果对一个变量执行 lock 操作,将会情况工作内存中的值,在执行引擎使用这个这个变量前,需要重新执行 load 或 assign 操作对 变量进行初始化。
  • 如果一个变量实现没有被 lock 操作锁定,那就不会允许对他执行 unlock 操作,也不允许去 unlock 一个被其它线程锁定的变量。
  • 对一个变量执行 unlock 操作前,必须先把此变量同步回主内存中(执行 store ,write 操作)。

JMM对volatile变量定义的特殊规则

Java内存模型中对volatile变量定义的特殊规则定义。假定T表示一个线程,V和W分别表示两个volatile型变量,那么在进行read、load、use、assign、store 和 write 操作时需要满足如下规则:

  • 只有当线程T对变量V执行的前一个动作是 load 的时候,线程T才能对变量V执行use动作;并且,只有当线程 T 对变量 V 执行的后一个动作是 use 的时候,线程T才能对变量V执行load 动作。线程 T 对变量 V 的 load、read 动作相关联,必须连续且一起出现。

     这条规则要求在工作内存中,每次使用 V 前必须先从主内存刷新最新的值,用于保证能看见其它线程对变量V所作的修改。
    
  • 只有当线程 T 对变量 V 执行的前一个动作是 assign 的时候,线程 T 才能对变量V执行 store 动作;并且,只有当线程 T 对变量 V执行的后一个动作是 store 时候,线程T 才能对变量 V 执行 assign 动作。线程 T 对变量 V 的 assign 动作 可以认为是线程 T 对变量 V 的 store、write动作相关联的,必须连续且一起出现。

     这条规则要求在工作内存中,每次修改 V 后 都必须立刻同步回主内存中,用于保证其它线程可以看到子集对变量 V 所做的修改。
    
  • 假定动作A 是线程 T 对变量V实施的 use 或 assign 动作,假定动作 F 是和动作A相关联的 load 或 store 动作,假定动作 P 和动作 F 相应的对变 V 的 read 或 write 动作;与此类似,假定动作 B 是线程 T 对变量 W 实施的 use 或 assign 动作,假定动作 G 是和动作B 相关联的 load 或 store 动作,假定动作 Q是和动作G相应的对变量W的 read 或 write 动作。如果 A 先于B,那么 P先于Q。

     这条规则要求 volatile 修饰的变量不会被指令重排序优化,从而保证代码的执行顺序与程序的顺序相同。
    

先行发生原则(happens-before)

下面是Java内存模型下一些 "天然的" 先行发生关系。
  • 程序次序规则(Program Order Rule):在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。
  • 监视器锁规则:在监视器上的解锁(unlock)操作先行发生于后面对同一个锁的加锁(lock)操作。
  • volatile变量规则:对一个volatile变量的写操作先行发生于对该volatile变量的读操作。
  • 线程启动规则:线程上对 Thread.start 的调用先行发生于该线程的每一个动作。
  • 线程结束规则:线程中的任何操作都必须在其它线程检测到该线程已结束之前执行,或者从Thread.join 中返回,或者在调用 Thread.isAlive 时返回false。
  • 中断规则:当一个线程在另一个线程上调用 Interrupt 时,必须在被中断线程检测到 interrupt 调用之前执行(通过抛出 InterruptException,或者调用 isInterrupted 和 interrupted)。
  • 终结器规则:一个对象的初始化完成(构造函数执行完成)发行发生于它的 finalize() 方法的开始。
  • 传递性:如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,则操作 A 一定先行发生于操作 C 。

在这里插入图片描述

在Java类库中提供的其它 Happens-Before 排序包括:

  • 将一个元素放入一个线程安全的容器的操作先行发生于从该容器中获取这个元素的操作。
  • 在 CountDownLatch 上的倒数操作将在线程从闭锁上的 await 方法返回之前执行。
  • 释放 Semaphore 许可的操作将在从该 Semaphore 上获取一个许可之前执行。
  • Future 表示的任务的所有操作将在从Future.get 中返回之前执行。
  • 向Executor提交一个Runnable 或 Callable 的操作将在任务开始之前执行。
  • 一个线程到达CyclicBarrier 或 Exchanger 的操作将在其他到达该栅栏或交换点的线程被释放之前执行。如果 CyclicBarrier 使用一个栅栏操作,那么到达栅栏将在栅栏操作之前执行,而栅栏操作又会在线程从栅栏中释放之前执行。

内存屏障

内存屏障(也称内存栅栏,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的一个同步点,使得此点之前的所有读写操作操作都执行后才可以开始执行此点之后的操作),避免代码重排序。代码屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令是插入特点的内存屏障指令,volatile实现了Java内存模型中的可见性和有序性(禁重排),但volatile 不能保证原子性。

内存屏障之前的所有写操作都要写回主内存

内存屏障之后的所有读操作都能获得所有内存屏障之前写操作的最新结果(实现了可见性)

写屏障(Store Memory Barrier):告诉处理器在写屏障之前将所有存储在缓存(store bufferes)中的数据同步到主内存。也就是说当看到Store屏障指令,就必须把该指令之前所有写入指令执行完毕才能继续往下执行。

读屏障(Load Memory Barrier):处理器在读屏障之后的读操作,都在读屏障之后执行。也就是说在Load屏障指令之后就能够保证后面的读取数据指令一定能够读取到最新的数据。

因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。一句话:对一个volatile变量的写,先行发生于任意后续对这个volatile变量的读,也叫写后读。

分类

内存屏障粗分有两种,分别是 读屏障(Load Barrier) 和 写屏障(Store Barrier)

读屏障(Load Barrier):在读指令之前插入读屏障,让工作内存或CPU高速缓存当中的缓存数据失效,重新回到主内存中获取最新数据。

写屏障 (Store Barrier):在写指令之后插入写屏障,强制把写缓冲区的数据刷回到主内存中。

再来看看源码 :首先打开 Unsafe.class ,发现都是本地方法
在这里插入图片描述
再打开其实现类:Unsafe.cpp
在这里插入图片描述
orderAccess.hpp
在这里插入图片描述
从上面的的源码可以看到,两种内存屏障又可以细分为四种,其作用如下:

屏障类型指令说明说明
LoadLoadLoad1; LoadLoad; Load2保证 Load1 的读取操作在 Load2 及其之后的读取操作之前执行
StoreStoreStore1; StoreStore; Store2在 Store2 及其后的写入操作执行之前,保证 Store1 的写入操作已刷新到主内存
LoadStoreLoad1;LoadStore; Store2在 Store2 及其后的写入操作执行之前,保证 Load1 的读取操作已读取结束
StoreLoadStore1; StoreLoad; Load2在 Load2 及其后的读取操作执行之前 ,保证Store1 的写入操作已刷新回主内存

最后 orderAccess_linux_x86.inline.hpp

inline void OrderAccess: : loadload()	{ acquire(); }
inline void OrderAccess: : storestore() { release(); }
inline void OrderAccess: : loadstore() 	{ acquire(); }
inline void OrderAccess: : storeload()	{ fence(); }inline void OrderAccess::acquire() {volatile intptr_t local_dummy;    
#ifdef AMD64__asm__ volatile ("movq 0(%%rsp), %0" : "=r" (local_dummy) :: "memory");
#else__asm__ volatile ("movl 0(%%esp), %0" : "=r" (local_dummy) :: "memory");	
#endif // AMD64
}inline void OrderAccess::release() {// Avoid hitting the same cache-line from// different threads.volatile jint local_dummy = 0;
}inline void OrderAccess::fence() {if (os::is_MP()){// always use locked addl since mfence is sometimes expensive
#ifdef AMD64__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif}
}

volatile 变量

关键字 volatile 可以说是 Java 虚拟机提供的最轻量级的同步机制。

volatile 变量只能确保 可见性 和 有序性(指令禁重排),不能确保原子性。

volatile 变量通常用作某个操作完成,发生中断或者状态的标志。

当且仅当满足以下条件时,才应该使用 volatile 变量
- 对变量的写入操作不依赖变量的当前值或者可以保证只有单个线程更新变量的值。
- 改变量不会与其它状态变量一起纳入不变性条件中
- 在访问变量时不需要加锁

可见性

可见性:一个线程修改了这个变量,其它线程都是可以立即得知的。

案例

public class VisibilityTest {// 可见性测试volatile static boolean flag = false ;public static void main(String[] args) {Thread.yield(); // 阻塞当前线程,直到 thread执行完成new Thread( ()->{flag = true; // 修改为真}).start();// 等待修改线程结束while (Thread.activeCount() > 1)Thread.yeild();System.out.println("After changed");while (flag){// 空旋,如果 thread的修改 可见// 那么程序将一直 空旋}}
}

在这里插入图片描述
可以看到 thread 线程将 flag 修改为 true之后,main线程一直在运行,说明thread对volatile变量flag的修改,main线程可见。

原子性测试

原子性: 是指 一个操作是不可中断的,要么全部执行成功要么全部执行失败。

public class AtomicityTest {private  volatile static int num1 = 0;private  static int num2 = 0;public  static void increment1(){// num++ 操作并不是一个原子性的操作// num++ 是一个“读取-修改-写入”的操作序列,并且状态结果依赖于之前的状态num1++;}// 加锁的自增方法public  synchronized static void increment2(){num2++;}public static void main(String[] args) {// 启动 100个线程,每个线程对 num修改100次,理论上 num1 和 num2 都应该 为 10000for (int i = 0; i < 100 ; i++) {new Thread( ()->{for (int j = 0; j < 100; j++) {increment1();increment2();}}).start();}// 等待所有累加线程结束while (Thread.activeCount() > 1)Thread.yeild();System.out.println("num1 = " + num1);System.out.println("num2 = " + num2);}}

在这里插入图片描述
发现 num1并不是 10000,说明 volatile 并不能保证变量的原子性

使用Javap反编译 increment1的代码,可以看到num++操作在Class文件中是由 4 条字节码构成的(return指令并不是由 num++产生的),从字节码层面就可很容易分析出并发失败的原因:当 getstatic 指令把 num的值取到操作数栈的栈顶时,volatile关键字保证了num的值是正确的,但是在执行 iconst_1,iadd指令的时候,其它线程可能将 num的值修改了,此时操作数栈顶的值就变成了过期的数据,所以 putstatic 指令执行过后就可能将较小的num值同步回主内存中
在这里插入图片描述
在这里插入图片描述

	由于 volatile变量只能保证可见性,当在不符合以下两条规则的运算场景中,仍然需要通过加锁来保证原子性
(synchronized、java.util.concurrent中的锁或者原子类)来保证原子性:
- 运算结果不依赖变量当前值,或者能够保证只有单一的线程对变量进行修改
- 变量不与其它状态变量一起纳入不变性条件中

有序性(指令重排序)

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序不存在数据依赖关系,可以重排序;存在数据依赖关系,禁止重排序,并且重排后的指令绝对不能改变原有的串行语义!

1. 什么是指令重排序
禁止指令重排序是 volatile 变量的第二个作用,普通的变量只能保证在该方法执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与代码中的执行顺序一致。

int x = 1; 	①
int y = 2;	②
x = x + 1;	③ 
y = 3 * x;	④
// 我们想要的执行顺序是 ①②③④,但实际执行顺序可能是 ②①③④

重排序的分类和执行流程
在这里插入图片描述

重排序的分类和执行流程

编译器优化的重排序:编译器在不改变单线程串行语义的前提下,可以重新调整指令的执行顺序

指令级并行的重排序:处理器使用指令级并行技术来讲多条指令重叠执行,若不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺内存

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

数据依赖性:若两个操作访问同一变量,且这两个操作中有一个为写操作,此时两操作间就存在数据依赖性。

2. 指令重拍可能造成的影响
在这里插入图片描述
在这里插入图片描述

在单线程中,由于 线程内表现为串行,所以始终能确保最终执行结果和代码顺序的结果一致 ,所以不用担心指令重排会导致结果不正确;

下面三种情况,只要改变两个操作的执行顺序,程序的执行结果就会改变
在这里插入图片描述

案例

public class ReSortTest {volatile  static int num; // 使用volatile 禁止指令重排序volatile static boolean flag; // 保证可见性public  static  void set(){num = 6;flag = true;}public static void print(){while (flag) {num = num + 1;System.out.println("num = " + num);}}
}
//  如果允许指令重排序,就有可能出现,flag=true了(还没执行num = 6),就执行 num = num + 1;输出的就可能是 num = 1

volatile 实现原理

volatile 底层是通过内存屏障来实现有序性和可见性的

volatile读原理

在这里插入图片描述
在这里插入图片描述

volatile写原理
在这里插入图片描述

在这里插入图片描述

== 总结==
在这里插入图片描述

  1. volatile 读之后的 任何操作不能重排序到 volatile 读之前
  2. volatile 写之前的 任何操作 不能与 volatile 写重排序
  3. volatile 写不能 与 之后的 volatile 操作 重排序

部分内容摘抄至《深入理解Java虚拟机》

相关内容

热门资讯

汽车油箱结构是什么(汽车油箱结... 本篇文章极速百科给大家谈谈汽车油箱结构是什么,以及汽车油箱结构原理图解对应的知识点,希望对各位有所帮...
美国2年期国债收益率上涨15个... 原标题:美国2年期国债收益率上涨15个基点 美国2年期国债收益率上涨15个基...
嵌入式 ADC使用手册完整版 ... 嵌入式 ADC使用手册完整版 (188977万字)💜&#...
重大消息战皇大厅开挂是真的吗... 您好:战皇大厅这款游戏可以开挂,确实是有挂的,需要了解加客服微信【8435338】很多玩家在这款游戏...
盘点十款牵手跑胡子为什么一直... 您好:牵手跑胡子这款游戏可以开挂,确实是有挂的,需要了解加客服微信【8435338】很多玩家在这款游...
senator香烟多少一盒(s... 今天给各位分享senator香烟多少一盒的知识,其中也会对sevebstars香烟进行解释,如果能碰...
终于懂了新荣耀斗牛真的有挂吗... 您好:新荣耀斗牛这款游戏可以开挂,确实是有挂的,需要了解加客服微信8435338】很多玩家在这款游戏...
盘点十款明星麻将到底有没有挂... 您好:明星麻将这款游戏可以开挂,确实是有挂的,需要了解加客服微信【5848499】很多玩家在这款游戏...
总结文章“新道游棋牌有透视挂吗... 您好:新道游棋牌这款游戏可以开挂,确实是有挂的,需要了解加客服微信【7682267】很多玩家在这款游...
终于懂了手机麻将到底有没有挂... 您好:手机麻将这款游戏可以开挂,确实是有挂的,需要了解加客服微信【8435338】很多玩家在这款游戏...