jvm的happens-before原则

2024-03-15 00:38
文章标签 java jvm 原则 happens

本文主要是介绍jvm的happens-before原则,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

提到并发,通常首先想到是锁,其实对共享资源的互斥操作是一方面,在java中还有一方面是内存的可见性和顺序化,了解JMM的同学可能会更清楚些,内存可见性和顺序性同样非常重要,在这里简单提一下JMM模型,首先介绍一下SMP(对称多处理结构)如下图:


在计算机中缓存到处可见,我们知道cpu的运算速度非常快,而从内存、甚至磁盘的读取速度则相对慢了几个数量级,所以缓存起到的是一个缓冲的作用,提高cpu相对的运算效率。SMP中每个cpu都有自己的缓存并且对其他cpu不可见,同时多个cpu共同享有一个主内存,主内存还每个cpu的缓存通讯通过总线IO来实现,因此当cpu缓存中对于主存数据的副本改变时,要同步的通过IO总线来刷新主存的数据,保证其他cpu看见得数据是合法的。在JMM中每个线程都有自己的工作内存,对其他线程不可见,同时有一个主内存,共所有的线程共享,java中有个volatile关键字,是一种轻量级的同步,主要是用来实现内存的可见性。有volatile关键字修饰的变量,当在线程的工作内存发生变化的时候,会同时写回到主内存,其他线程读取的时候,也会强制从主内存重读,这就保证其他线程读到的数据是正确的。

下面看一张别人画的图:


上面就是提到的JMM模型,实际上每个线程都有自己的工作内存且只对自己可见,而这里的共享内存,一般指的也是java中的堆。

上面提到过,现代的处理器由于处理速度非常快,因此通常都会有一个写内存,先把值保存到自己的缓存中,找个合适的实际在刷新到共享内存,因此这里对内存的操作可能存在可见性的问题,举个例子:

Processor A Processor B
a = 1; //A1
x = b; //A2
b = 2; //B1
y = a; //B2
初始状态:a = b = 0
处理器允许执行后得到结果:x = y = 0
假设我们的代码如下:

a = 1;

b = 2;

x = b;

y = a;

其中a和b全为共享变量,可以理解是成员变量。

由于是多线程并发指向,完全可能出现上面表中的操作顺序。理论上即使是多线程也会得到x = 2;y = 1的结果(这里并没有数据争用),但是有可能会发生下面的情况:


处理器A(也可理解为线程A)的操作顺序是A1,A2,A3,处理器B的操作顺序是B1,B2,B3。

1.处理器A先把a=1写到自己的缓冲区,注意此时共享内存的a仍为0,于此同时处理B把b=2写到自己的缓冲区,但此时共享内存的b还是0。

2.处理器A从共享内存读取b的值,并赋值给x,于此同时处理器B从共享内存读取a的值,赋值给y。此时x = y = 0;

3.处理器A和B分别把自己缓冲区的值刷新到共享内存。

从代码层面看处理器A质性的是A1->A2,但是从内存可见性看,执行完刷新共享内存a的写入才算完成。因此这里的实际质性顺序是A2->A1,因此这里的指令被重排序了。因为大多数啊处理器都应用到了写缓冲区,所以重排序的特性很常见。

JMM针对这种重排序的特性会生成内存屏障指令来阻止某种程度的重排序,从而保证内存的可见性,cpu为了提高执行速度,会对我们的代码(编译后生成的指令)进行重排序,因此代码的执行顺序并不重要,只要我们的最终结果正确就行,因此有时候为了程序的正确性,jvm不得不作出某些动作来保证结果的可见性,这其中包括下面几种:

屏障类型指令示例说明
LoadLoad BarriersLoad1; LoadLoad; Load2确保Load1数据的装载,之前于Load2及所有后续装载指令的装载。
StoreStore BarriersStore1; StoreStore; Store2确保Store1数据对其他处理器可见(刷新到内存),之前于Store2及所有后续存储指令的存储。
LoadStore BarriersLoad1; LoadStore; Store2确保Load1数据装载,之前于Store2及所有后续的存储指令刷新到内存。
StoreLoad BarriersStore1; StoreLoad; Load2确保Store1数据对其他处理器变得可见(指刷新到内存),之前于Load2及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。
以第一个load-load为例子,该指令确保load1的操作在load2及其之后的所有load操作被执行前,执行,且保证load的值对所有的处理器可见。

实际上java中的voilate原语就是阻止对指令的重排序,volatile变量在写操作之后会插入一个store屏障,在读操作之前会插入一个load屏障。(也就是说对于volatile变量,如果有线程修改了它的值,该值会马上对其他线程可见,并且一个线程读取该值的时候,其他线程缓存中的值会被同步刷新到最新值)。一个类的final字段会在初始化后插入一个store屏障,来确保final字段在构造函数初始化完成并可被使用时可见。因此volatile使用需要谨慎,用的不好会造成性能的浪费(频繁的通过总线刷新各个处理器的值,可能造成数据风暴)。

为了简化这种可见性,java中有个happens-before规则,它描述了同一个线程或者不同线程的某个操作的结果,对另外一个操作可见性的原则。happens-before实际上就是定义在各种action上的一种偏序关系。所谓的action包括变量的读写,监视器的加锁和解锁,线程的启动和拼接等等。

happens-before规则如下:

程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。 
监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。 
volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。 
传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。 
Thread.start()的调用会happens-before于启动线程里面的动作。 
Thread中的所有动作都happens-before于其他线程从Thread.join中成功返回。

解释一下第一条,在单线程中,一个线程的操作对该操作后续的所有操作都可见(注意happens-before描述的是可见性规则,并不是顺序),该规则也保证了单线程中程序的正确执行。再来看下第二条,并不是讲解锁操作后再加锁,而是讲一个线程释放某个锁的时候,在释放锁或它之前的操作都对另外一个锁定该锁的线程(也可以是一个线程)可见。

上面只是列了几个规则,实际可能不止这些,如果不满足上面的规则,则需要考虑使用同步等方法,来强制满足。

另外上面的几个规则是基于java的内存模型给出的,在java语言层面也给出了很多happens-before原则,比如ReentrantLock的unlock与lock操作,又如AbstractQueuedSynchronizer的release与acquire,setState与getState等等。

happens-before简化了并发编程的难度,了解它的含义多少对我们有些好处。附上一张java的内存模型图:


这篇关于jvm的happens-before原则的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


原文地址:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.chinasem.cn/article/810255

相关文章

SpringBoot应用中出现的Full GC问题的场景与解决

《SpringBoot应用中出现的FullGC问题的场景与解决》这篇文章主要为大家详细介绍了SpringBoot应用中出现的FullGC问题的场景与解决方法,文中的示例代码讲解详细,感兴趣的小伙伴可... 目录Full GC的原理与触发条件原理触发条件对Spring Boot应用的影响示例代码优化建议结论F

springboot项目中常用的工具类和api详解

《springboot项目中常用的工具类和api详解》在SpringBoot项目中,开发者通常会依赖一些工具类和API来简化开发、提高效率,以下是一些常用的工具类及其典型应用场景,涵盖Spring原生... 目录1. Spring Framework 自带工具类(1) StringUtils(2) Coll

SpringBoot条件注解核心作用与使用场景详解

《SpringBoot条件注解核心作用与使用场景详解》SpringBoot的条件注解为开发者提供了强大的动态配置能力,理解其原理和适用场景是构建灵活、可扩展应用的关键,本文将系统梳理所有常用的条件注... 目录引言一、条件注解的核心机制二、SpringBoot内置条件注解详解1、@ConditionalOn

通过Spring层面进行事务回滚的实现

《通过Spring层面进行事务回滚的实现》本文主要介绍了通过Spring层面进行事务回滚的实现,包括声明式事务和编程式事务,具有一定的参考价值,感兴趣的可以了解一下... 目录声明式事务回滚:1. 基础注解配置2. 指定回滚异常类型3. ​不回滚特殊场景编程式事务回滚:1. ​使用 TransactionT

Spring LDAP目录服务的使用示例

《SpringLDAP目录服务的使用示例》本文主要介绍了SpringLDAP目录服务的使用示例... 目录引言一、Spring LDAP基础二、LdapTemplate详解三、LDAP对象映射四、基本LDAP操作4.1 查询操作4.2 添加操作4.3 修改操作4.4 删除操作五、认证与授权六、高级特性与最佳

Spring Shell 命令行实现交互式Shell应用开发

《SpringShell命令行实现交互式Shell应用开发》本文主要介绍了SpringShell命令行实现交互式Shell应用开发,能够帮助开发者快速构建功能丰富的命令行应用程序,具有一定的参考价... 目录引言一、Spring Shell概述二、创建命令类三、命令参数处理四、命令分组与帮助系统五、自定义S

SpringSecurity JWT基于令牌的无状态认证实现

《SpringSecurityJWT基于令牌的无状态认证实现》SpringSecurity中实现基于JWT的无状态认证是一种常见的做法,本文就来介绍一下SpringSecurityJWT基于令牌的无... 目录引言一、JWT基本原理与结构二、Spring Security JWT依赖配置三、JWT令牌生成与

Java中Date、LocalDate、LocalDateTime、LocalTime、时间戳之间的相互转换代码

《Java中Date、LocalDate、LocalDateTime、LocalTime、时间戳之间的相互转换代码》:本文主要介绍Java中日期时间转换的多种方法,包括将Date转换为LocalD... 目录一、Date转LocalDateTime二、Date转LocalDate三、LocalDateTim

如何配置Spring Boot中的Jackson序列化

《如何配置SpringBoot中的Jackson序列化》在开发基于SpringBoot的应用程序时,Jackson是默认的JSON序列化和反序列化工具,本文将详细介绍如何在SpringBoot中配置... 目录配置Spring Boot中的Jackson序列化1. 为什么需要自定义Jackson配置?2.

Java中使用Hutool进行AES加密解密的方法举例

《Java中使用Hutool进行AES加密解密的方法举例》AES是一种对称加密,所谓对称加密就是加密与解密使用的秘钥是一个,下面:本文主要介绍Java中使用Hutool进行AES加密解密的相关资料... 目录前言一、Hutool简介与引入1.1 Hutool简介1.2 引入Hutool二、AES加密解密基础