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.

相关文章
|
23天前
|
安全 Java 应用服务中间件
Spring Boot + Java 21:内存减少 60%,启动速度提高 30% — 零代码
通过调整三个JVM和Spring Boot配置开关,无需重写代码即可显著优化Java应用性能:内存减少60%,启动速度提升30%。适用于所有在JVM上运行API的生产团队,低成本实现高效能。
152 3
|
2月前
|
存储 缓存 Java
Java数组全解析:一维、多维与内存模型
本文深入解析Java数组的内存布局与操作技巧,涵盖一维及多维数组的声明、初始化、内存模型,以及数组常见陷阱和性能优化。通过图文结合的方式帮助开发者彻底理解数组本质,并提供Arrays工具类的实用方法与面试高频问题解析,助你掌握数组核心知识,避免常见错误。
|
1月前
|
缓存 监控 Kubernetes
Java虚拟机内存溢出(Java Heap Space)问题处理方案
综上所述, 解决Java Heap Space溢出需从多角度综合施策; 包括但不限于配置调整、代码审查与优化以及系统设计层面改进; 同样也不能忽视运行期监控与预警设置之重要性; 及早发现潜在风险点并采取相应补救手段至关重要.
271 17
|
5月前
|
存储 缓存 Java
【高薪程序员必看】万字长文拆解Java并发编程!(5):深入理解JMM:Java内存模型的三大特性与volatile底层原理
JMM,Java Memory Model,Java内存模型,定义了主内存,工作内存,确保Java在不同平台上的正确运行主内存Main Memory:所有线程共享的内存区域,所有的变量都存储在主存中工作内存Working Memory:每个线程拥有自己的工作内存,用于保存变量的副本.线程执行过程中先将主内存中的变量读到工作内存中,对变量进行操作之后再将变量写入主内存,jvm概念说明主内存所有线程共享的内存区域,存储原始变量(堆内存中的对象实例和静态变量)工作内存。
173 0
|
2月前
|
监控 Kubernetes Java
最新技术栈驱动的 Java 绿色计算与性能优化实操指南涵盖内存优化与能效提升实战技巧
本文介绍了基于Java 24+技术栈的绿色计算与性能优化实操指南。主要内容包括:1)JVM调优,如分代ZGC配置和结构化并发优化;2)代码级优化,包括向量API加速数据处理和零拷贝I/O;3)容器化环境优化,如K8s资源匹配和节能模式配置;4)监控分析工具使用。通过实践表明,这些优化能显著提升性能(响应时间降低40-60%)同时降低资源消耗(内存减少30-50%,CPU降低20-40%)和能耗(服务器功耗减少15-35%)。建议采用渐进式优化策略。
143 1
|
2月前
|
存储 监控 算法
Java垃圾回收机制(GC)与内存模型
本文主要讲述JVM的内存模型和基本调优机制。
|
4月前
|
Java 物联网 数据处理
Java Solon v3.2.0 史上最强性能优化版本发布 并发能力提升 700% 内存占用节省 50%
Java Solon v3.2.0 是一款性能卓越的后端开发框架,新版本并发性能提升700%,内存占用节省50%。本文将从核心特性(如事件驱动模型与内存优化)、技术方案示例(Web应用搭建与数据库集成)到实际应用案例(电商平台与物联网平台)全面解析其优势与使用方法。通过简单代码示例和真实场景展示,帮助开发者快速掌握并应用于项目中,大幅提升系统性能与资源利用率。
137 6
Java Solon v3.2.0 史上最强性能优化版本发布 并发能力提升 700% 内存占用节省 50%
|
2月前
|
边缘计算 算法 Java
Java 绿色计算与性能优化:从内存管理到能耗降低的全方位优化策略与实践技巧
本文探讨了Java绿色计算与性能优化的技术方案和应用实例。文章从JVM调优(包括垃圾回收器选择、内存管理和并发优化)、代码优化(数据结构选择、对象创建和I/O操作优化)等方面提出优化策略,并结合电商平台、社交平台和智能工厂的实际案例,展示了通过Java新特性提升性能、降低能耗的显著效果。最终指出,综合运用这些优化方法不仅能提高系统性能,还能实现绿色计算目标,为企业节省成本并符合环保要求。
97 0
|
4月前
|
缓存 监控 Cloud Native
Java Solon v3.2.0 高并发与低内存实战指南之解决方案优化
本文深入解析了Java Solon v3.2.0框架的实战应用,聚焦高并发与低内存消耗场景。通过响应式编程、云原生支持、内存优化等特性,结合API网关、数据库操作及分布式缓存实例,展示其在秒杀系统中的性能优势。文章还提供了Docker部署、监控方案及实际效果数据,助力开发者构建高效稳定的应用系统。代码示例详尽,适合希望提升系统性能的Java开发者参考。
183 4
Java Solon v3.2.0 高并发与低内存实战指南之解决方案优化