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

相关文章

在Ubuntu上部署SpringBoot应用的操作步骤

《在Ubuntu上部署SpringBoot应用的操作步骤》随着云计算和容器化技术的普及,Linux服务器已成为部署Web应用程序的主流平台之一,Java作为一种跨平台的编程语言,具有广泛的应用场景,本... 目录一、部署准备二、安装 Java 环境1. 安装 JDK2. 验证 Java 安装三、安装 mys

Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单

《Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单》:本文主要介绍Springboot的ThreadPoolTaskScheduler线... 目录ThreadPoolTaskScheduler线程池实现15分钟不操作自动取消订单概要1,创建订单后

JAVA中整型数组、字符串数组、整型数和字符串 的创建与转换的方法

《JAVA中整型数组、字符串数组、整型数和字符串的创建与转换的方法》本文介绍了Java中字符串、字符数组和整型数组的创建方法,以及它们之间的转换方法,还详细讲解了字符串中的一些常用方法,如index... 目录一、字符串、字符数组和整型数组的创建1、字符串的创建方法1.1 通过引用字符数组来创建字符串1.2

SpringCloud集成AlloyDB的示例代码

《SpringCloud集成AlloyDB的示例代码》AlloyDB是GoogleCloud提供的一种高度可扩展、强性能的关系型数据库服务,它兼容PostgreSQL,并提供了更快的查询性能... 目录1.AlloyDBjavascript是什么?AlloyDB 的工作原理2.搭建测试环境3.代码工程1.

Java调用Python代码的几种方法小结

《Java调用Python代码的几种方法小结》Python语言有丰富的系统管理、数据处理、统计类软件包,因此从java应用中调用Python代码的需求很常见、实用,本文介绍几种方法从java调用Pyt... 目录引言Java core使用ProcessBuilder使用Java脚本引擎总结引言python

SpringBoot操作spark处理hdfs文件的操作方法

《SpringBoot操作spark处理hdfs文件的操作方法》本文介绍了如何使用SpringBoot操作Spark处理HDFS文件,包括导入依赖、配置Spark信息、编写Controller和Ser... 目录SpringBoot操作spark处理hdfs文件1、导入依赖2、配置spark信息3、cont

springboot整合 xxl-job及使用步骤

《springboot整合xxl-job及使用步骤》XXL-JOB是一个分布式任务调度平台,用于解决分布式系统中的任务调度和管理问题,文章详细介绍了XXL-JOB的架构,包括调度中心、执行器和Web... 目录一、xxl-job是什么二、使用步骤1. 下载并运行管理端代码2. 访问管理页面,确认是否启动成功

Java中的密码加密方式

《Java中的密码加密方式》文章介绍了Java中使用MD5算法对密码进行加密的方法,以及如何通过加盐和多重加密来提高密码的安全性,MD5是一种不可逆的哈希算法,适合用于存储密码,因为其输出的摘要长度固... 目录Java的密码加密方式密码加密一般的应用方式是总结Java的密码加密方式密码加密【这里采用的

Java中ArrayList的8种浅拷贝方式示例代码

《Java中ArrayList的8种浅拷贝方式示例代码》:本文主要介绍Java中ArrayList的8种浅拷贝方式的相关资料,讲解了Java中ArrayList的浅拷贝概念,并详细分享了八种实现浅... 目录引言什么是浅拷贝?ArrayList 浅拷贝的重要性方法一:使用构造函数方法二:使用 addAll(

解决mybatis-plus-boot-starter与mybatis-spring-boot-starter的错误问题

《解决mybatis-plus-boot-starter与mybatis-spring-boot-starter的错误问题》本文主要讲述了在使用MyBatis和MyBatis-Plus时遇到的绑定异常... 目录myBATis-plus-boot-starpythonter与mybatis-spring-b