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

相关文章

Maven中引入 springboot 相关依赖的方式(最新推荐)

《Maven中引入springboot相关依赖的方式(最新推荐)》:本文主要介绍Maven中引入springboot相关依赖的方式(最新推荐),本文给大家介绍的非常详细,对大家的学习或工作具有... 目录Maven中引入 springboot 相关依赖的方式1. 不使用版本管理(不推荐)2、使用版本管理(推

Java 中的 @SneakyThrows 注解使用方法(简化异常处理的利与弊)

《Java中的@SneakyThrows注解使用方法(简化异常处理的利与弊)》为了简化异常处理,Lombok提供了一个强大的注解@SneakyThrows,本文将详细介绍@SneakyThro... 目录1. @SneakyThrows 简介 1.1 什么是 Lombok?2. @SneakyThrows

在 Spring Boot 中实现异常处理最佳实践

《在SpringBoot中实现异常处理最佳实践》本文介绍如何在SpringBoot中实现异常处理,涵盖核心概念、实现方法、与先前查询的集成、性能分析、常见问题和最佳实践,感兴趣的朋友一起看看吧... 目录一、Spring Boot 异常处理的背景与核心概念1.1 为什么需要异常处理?1.2 Spring B

如何在 Spring Boot 中实现 FreeMarker 模板

《如何在SpringBoot中实现FreeMarker模板》FreeMarker是一种功能强大、轻量级的模板引擎,用于在Java应用中生成动态文本输出(如HTML、XML、邮件内容等),本文... 目录什么是 FreeMarker 模板?在 Spring Boot 中实现 FreeMarker 模板1. 环

SpringMVC 通过ajax 前后端数据交互的实现方法

《SpringMVC通过ajax前后端数据交互的实现方法》:本文主要介绍SpringMVC通过ajax前后端数据交互的实现方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价... 在前端的开发过程中,经常在html页面通过AJAX进行前后端数据的交互,SpringMVC的controll

Java中的工具类命名方法

《Java中的工具类命名方法》:本文主要介绍Java中的工具类究竟如何命名,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录Java中的工具类究竟如何命名?先来几个例子几种命名方式的比较到底如何命名 ?总结Java中的工具类究竟如何命名?先来几个例子JD

Java Stream流使用案例深入详解

《JavaStream流使用案例深入详解》:本文主要介绍JavaStream流使用案例详解,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录前言1. Lambda1.1 语法1.2 没参数只有一条语句或者多条语句1.3 一个参数只有一条语句或者多

Spring Security自定义身份认证的实现方法

《SpringSecurity自定义身份认证的实现方法》:本文主要介绍SpringSecurity自定义身份认证的实现方法,下面对SpringSecurity的这三种自定义身份认证进行详细讲解,... 目录1.内存身份认证(1)创建配置类(2)验证内存身份认证2.JDBC身份认证(1)数据准备 (2)配置依

SpringBoot整合OpenFeign的完整指南

《SpringBoot整合OpenFeign的完整指南》OpenFeign是由Netflix开发的一个声明式Web服务客户端,它使得编写HTTP客户端变得更加简单,本文为大家介绍了SpringBoot... 目录什么是OpenFeign环境准备创建 Spring Boot 项目添加依赖启用 OpenFeig

Java Spring 中 @PostConstruct 注解使用原理及常见场景

《JavaSpring中@PostConstruct注解使用原理及常见场景》在JavaSpring中,@PostConstruct注解是一个非常实用的功能,它允许开发者在Spring容器完全初... 目录一、@PostConstruct 注解概述二、@PostConstruct 注解的基本使用2.1 基本代