Java内存模型详解

简介: 该文章主要介绍了Java内存模型的相关概念和技术细节,包括Java内存模型的定义、缓存一致性策略、内存交互操作、内存屏障等。

一、Java内存模型定义:为了解决屏蔽掉各种硬件和操作系统的内存访问差异,对内存与高速缓存进行读写操作的过程抽象。

目前大多数硬件采用的缓存一致性策略或协议是MESI或基于MESI的变种:

M代表更改(modified),表示缓存中的数据已经更改,在未来的某个时刻将会写入内存;

E代表排除(exclusive),表示缓存的数据只被当前的核心所缓存;

S代表共享(shared),表示缓存的数据还被其他核心缓存;

I代表无效(invalid),表示缓存中的数据已经失效,即其他核心更改了数据。

二、JMM内存中定义了8种内存交互操作,每种操作都是原子的。

内存操作 作用范围 作用
lock 主内存的变量 把一个变量标识为线程独占状态
unlock 主内存的变量 将锁定状态变量释放出来
read 主内存变量 将主内存变量同步到工作内存
load 工作内存变量 将read操作从主内存得到的变量值放入工作内存变量副本
use 作用用工作内存的变量 把工作内存中变量副本传递给执行引擎
assign 作用于工作内存的变量 从执行引擎接收到的变量赋给工作内存变量
store 作用于工作内存变 把工作内存变量传到主内存,便于write操作使用
write 作用于主内存的变量 把store操作从工作内存中得到的变量的值放入主内存的变量中

如果要完成主内存复制到工作内存,则需要read与load顺序执行,不需要连续执行

如果要完成工作内存同步到主内存,则需要store与write顺序执行,不需要连续执行

还有如下规定:

1、read/load,store/write不能单独出现

2、不允许线程丢弃assign行为

3、lock一个时刻只能有1个线程执行成功,lock与unlock必须执行次数相同

4、lock执行后,工作内存的值会清空,执行引擎使用该变量时需要执行read/load操作从主内存读取,也就是sychronized操作为什么可以保证可见性的原因。

5、unlock之前必须把变量同步到主内存,也是sychronized可以实现线程可见性原因。

三、再说volatile在JMM中的特殊规则

1)修改volatile变量时会强制将修改后的值刷新的主内存中。 普通变量做不到

2)修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。 普通变量做不到

因此,对于volatile修饰的变量,如果只是读取操作,可以保证线程直接可见性。对于a++这种非原子操作并不保证线程安全。

volatile 对long,double64位的类型数据赋值操作具有原子性 普通变量对这种类型读写操作会进行分高32位低32位处理 修饰为volatile后,就使这种操作有了原子性 。但是对于这种变量自增/自减操作是不保证原子性的

17.7双长的非原子处理在Java编程语言内存模型中,对非易失长值或双值的单次写入被视为两次单独的写入:每32位一半写入一次,目前虚拟机都选择把64位数据的读写操作作为原子操作对待。

第二 禁止指令重排序 通过内存屏障 hapen-before 原则

编译volatile变量赋值的代码可知道,加入了lock 指令前缀(cpu级别的指令)

它有两个作用:

使得当前修改的变量立即同步到主内存

其他cpu持有的变量副本失效

volatile在不满足以下两种场景的情况下就可以使用:

1、对该变量读取后的操作,不依赖原来的值

2、不需要与其他状态变量来维护不变约束

JMM规定了对volatile指令的内存操作顺序行为来保证可见性:

场景:线程T,V,W都是修饰成volatile的变量

1、load V use V必须连续出现,而read load又是依赖出现,所以表现为read 2、load use V必须连续出现 。这样就是使用V之前必须从主内存读取V的值,使用volatile变量时都是从主内存读取使用,不是直接从工作内存读取使用。

3、assign store 必须连续使用,而store write操作又是依赖出现,也就是每次修改volatile变量后,都必须立即刷新到主内存。

4、T分别对V进行use assign 操作A, W进行use assign 操作B,如果A早于B

那么T对V进行read操作P,W进行read操作Q,那么P早于Q这样就保证volatile修饰的变量不会被指令重排序优化。

四、 并发编程的三个问题

原子性:read,load,assIgin等指令都是原子性的。

而lock,unlock指令提供了原子性支持,jvm体现在字节码monitorenter于monitorexit隐式使用

程序中使用sychronized来实现该语义。

可见性,volatile,sychronized(unlock执行前,必须将变量写会主内存),final也能实现可见性

有序性:一个线程内操作都是有序的,线程内串行,如果存在多个线程共享变量,则会由于指令重排序优化与工作内存与主内存存在延迟,volatile,sychronized(一个变量在同一个时刻只允许一条线程对其lock操作,必须排队)

五、Java中happen-before原则,java中无需同步机制就可以保证的先行发生原则:判断数据是否有竞争,线程是否安全的依据:

1、程序次序规则 。控制流顺序,写在前面的先执行

2、管程锁定规则:同一个锁的unlock必须发生于后面同一个锁的lock操作前

3、volatile规则 代码中 volatile写操作必须早于后面的读取操作

4、Thread start方法会比这个线程其他动作早执行

5、Thread stop 结束线程前线程操作都执行完毕,Thread.join方法,Thread.isAlive方法探测线程是否结束。

6、Thread interrupt()方法早于interupted()方法(检测线程中断事件)

7、对象构造函数早于finalize方法执行前

8、传递性,如果a早于b,b早于c,则a早于c

9、先行发生原则与时间顺序无关。

Java的内存屏障通常所谓的四种即LoadLoad,StoreStore,LoadStore,StoreLoad实际上也是Load,Store两种的组合,完成一系列的屏障和数据同步功能。

LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

LoadLoad Load1(s); LoadLoad; Load2 在加载2和任何后续加载操作之前 确保Load1完成(获取从内存加载的值)
StoreStore Store1(s); StoreStore; Store2 在存储2和任何后续存储确保Store1已经完成(对Store1的内存产生影响对其他处理器可见,volatile实现了内存可见性原因)
LoadStore Load1(s); LoadStore; Store2 确保Load1在Store2和任何后续存储之前完成//操作
StoreLoad Store1(s); StoreLoad; Load2 确保Store1完成操作在Load2和任何后续加载之前
fence 我们将“栅栏”操作定义为双向屏障。它保证在围栏之前的任何内存访问都不会重新排序,程序中围栏之后的任何内存访问命令。这可用于防止指令重排序

Java中又定义了release和acquire,fence三种不同的语境的内存栅栏.

如上图,loadLoad和loadStore两种栅栏对应的都是acquire语境,,acquire语境一般定义在java的读之前;在编译器阶段和cpu执行的时候,acquire之后的所有的(读和写)操作不能越过acquire,重排到acquire之前,acquire指令之后所有的读都是具有可见性的.

如上图,StoreStore和LoadStore对应的是release语境,release语境一般定义在java的写之后,在编译器和cpu执行的时候,所有release之前的所有的(读和写)操作都不能越过release,重排到release之后,release指令之前所有的写都会刷新到主存中去,其他核的cpu可以看到刷新的最新值.

对于fence,是由storeload栅栏组成的,比较消耗性能.在编译器阶段和cpu执行时候,保证fence之前的任何操作不能重排到屏障之后,fence之后的任何操作不能重排到屏障之前.fence具有acquire和release这两个都有的语境,即可以将fence之前的写刷新到内存中,fence之后的读都是具有可见性的.

内存屏障,也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。语义上,内存屏障之前的所有写操作都要写入主存;内存屏障之后的读操作,直接读取的主存,可以获得内存屏障之前的写操作的结果。

完全内存屏障(full memory barrier)保障了早于屏障的内存读写操作的结果提交到内存之后,再执行晚于屏障的读写操作,在loadbuffer和storebuffer中插入屏障,清空屏障之前的读和写操作。X86中对应MFence;

内存读屏障(read memory barrier)仅确保了内存读操作.在loadbuffe中插入屏障,清空屏障之前的读操作;LFence

内存写屏障(write memory barrier)仅保证了内存写操作.在storebuffer中插入屏障,清空屏障之的写操作; SFence

通过查看hotspot源码可知,使用了伪完全屏障与编译器屏障

inline void OrderAccess::loadload() { compiler_barrier(); }

inline void OrderAccess::storestore() { compiler_barrier(); }

inline void OrderAccess::loadstore() { compiler_barrier(); }

inline void OrderAccess::storeload() { fence(); }

inline void OrderAccess::acquire() { compiler_barrier(); }

inline void OrderAccess::release() { compiler_barrier(); }

// A compiler barrier, forcing the C++ compiler to invalidate all memory //assumptions 使得所有缓存失效

static inline void compiler_barrier()

{

asm volatile ("" : : : "memory");

}

inline void OrderAccess::fence() {

// always use locked addl since mfence is sometimes expensive

ifdef AMD64

asm volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");

else

// lock cpu级别汇编指令两个作用:

1、使得当前处理器缓存数据同步到主内存,

2、其他cpu核心缓存中数据无效,实现缓存一致性问题

asm volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");

endif

compiler_barrier();

}

解释一下: asm :代表汇编代码开始.

volatile:禁止编译器对代码进行某些优化.

Lock :汇编代码,让后面的操作是原子操作.lock指令会锁住操作的缓存行(cacheline),一般用于read-Modify-write的操作例如 addl.“cc”,”mmeory”:cc代表的是寄存器,memory代表是内存;这边同时用了”cc”和”memory”,来通知编译器内存或者寄存器内的内容已经发生了修改,要重新生成加载指令(不可以从缓存寄存器中取).

这边的read/write请求不能越过lock指令进行重排,那么所有带有lock prefix指令(lock ,xchgl等)都会构成一个天然的x86 Mfence(读写屏障),这里用lock指令作为内存屏障,然后利用asm volatile("" ::: "cc,memory")作为编译器屏障.

AMD64这边判断是否是64位,64位机器中使用rsp栈指针寄存器,32位机器中使用32位机器中使用esp栈指针寄存器.

可以看到jvm开发组没有使用x86的内存屏障指令(mfence,lfence,sfence)

volatile写操作实现:

inline void oopDesc::obj_field_put_volatile(int offset, oop value) {
  OrderAccess::release();
  obj_field_put(offset, value);
  OrderAccess::fence();
}
inline volatile oop oopDesc::obj_field_volatile(int offset) const {
  volatile oop value = obj_field(offset);
  OrderAccess::acquire();
  return value;
}

引用官方说明

// According to the new Java Memory Model (JMM):

// (1) All volatiles are serialized wrt to each other. ALSO reads &

// writes act as aquire & release, so:

// (2) A read cannot let unrelated NON-volatile memory refs that

// happen after the read float up to before the read. It's OK for

// non-volatile memory refs that happen before the volatile read to

// float down below it.

// (3) Similar a volatile write cannot let unrelated NON-volatile

// memory refs that happen BEFORE the write float down to after the

// write. It's OK for non-volatile memory refs that happen after the

// volatile write to float up before it.

//

// We only put in barriers around volatile refs (they are expensive),

// not between memory refs (that would require us to track the

// flavor of the previous memory refs). Requirements (2) and (3)

// require some barriers before volatile stores and after volatile

// loads. These nearly cover requirement (1) but miss the

// volatile-store-volatile-load case. This final case is placed after

// volatile-stores although it could just as well go before

// volatile-loads.

相关文章
|
24天前
|
存储 缓存 安全
Java内存模型深度解析:从理论到实践####
【10月更文挑战第21天】 本文深入探讨了Java内存模型(JMM)的核心概念与底层机制,通过剖析其设计原理、内存可见性问题及其解决方案,结合具体代码示例,帮助读者构建对JMM的全面理解。不同于传统的摘要概述,我们将直接以故事化手法引入,让读者在轻松的情境中领略JMM的精髓。 ####
33 6
|
15天前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
21 0
|
25天前
|
存储 算法 Java
Java内存管理深度剖析与优化策略####
本文深入探讨了Java虚拟机(JVM)的内存管理机制,重点分析了堆内存的分配策略、垃圾回收算法以及如何通过调优提升应用性能。通过案例驱动的方式,揭示了常见内存泄漏的根源与解决策略,旨在为开发者提供实用的内存管理技巧,确保应用程序既高效又稳定地运行。 ####
|
17天前
|
存储 监控 算法
Java内存管理深度剖析:从垃圾收集到内存泄漏的全面指南####
本文深入探讨了Java虚拟机(JVM)中的内存管理机制,特别是垃圾收集(GC)的工作原理及其调优策略。不同于传统的摘要概述,本文将通过实际案例分析,揭示内存泄漏的根源与预防措施,为开发者提供实战中的优化建议,旨在帮助读者构建高效、稳定的Java应用。 ####
31 8
|
15天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
19天前
|
存储 算法 Java
Java 内存管理与优化:掌控堆与栈,雕琢高效代码
Java内存管理与优化是提升程序性能的关键。掌握堆与栈的运作机制,学习如何有效管理内存资源,雕琢出更加高效的代码,是每个Java开发者必备的技能。
46 5
|
17天前
|
存储 算法 Java
Java内存管理深度解析####
本文深入探讨了Java虚拟机(JVM)中的内存分配与垃圾回收机制,揭示了其高效管理内存的奥秘。文章首先概述了JVM内存模型,随后详细阐述了堆、栈、方法区等关键区域的作用及管理策略。在垃圾回收部分,重点介绍了标记-清除、复制算法、标记-整理等多种回收算法的工作原理及其适用场景,并通过实际案例分析了不同GC策略对应用性能的影响。对于开发者而言,理解这些原理有助于编写出更加高效、稳定的Java应用程序。 ####
|
17天前
|
安全 Java 程序员
Java内存模型的深入理解与实践
本文旨在深入探讨Java内存模型(JMM)的核心概念,包括原子性、可见性和有序性,并通过实例代码分析这些特性在实际编程中的应用。我们将从理论到实践,逐步揭示JMM在多线程编程中的重要性和复杂性,帮助读者构建更加健壮的并发程序。
|
22天前
|
算法 Java 开发者
Java内存管理与垃圾回收机制深度剖析####
本文深入探讨了Java虚拟机(JVM)的内存管理机制,特别是其垃圾回收机制的工作原理、算法及实践优化策略。不同于传统的摘要概述,本文将以一个虚拟的“城市环卫系统”为比喻,生动形象地揭示Java内存管理的奥秘,旨在帮助开发者更好地理解并调优Java应用的性能。 ####
|
23天前
|
Java
java内存区域
1)栈内存:保存所有的对象名称 2)堆内存:保存每个对象的具体属性 3)全局数据区:保存static类型的属性 4)全局代码区:保存所有的方法定义
21 1