Java 注解机制解密并发编程的时间之谜:揭开Happens-Before的神秘面纱

优质博文:IT-BLOG-CN

一、简介

为什么需要happens-before原则: 主要是因为Java内存模型 , 为了提高CPU效率,通过工作内存Cache代替了主内存。修改这个临界资源会更新work memory但并不一定立刻刷到主存中。通常JMM会将编写的代码编译后执行,在编译器中生成的指令的顺序跟源码的顺序并不是完全一致的。处理器可能采用乱序或者并行的方式来执行指令,因为在JVM中只要程序的最终结果一致,这种重排序是允许的。并且处理器还有本地缓存,当将结果存储在本地缓存中,其他线程是无法看到结果的。除此之外缓存提交到主内存的顺序也肯能会变化。在多线程环境下可能会产生不同的结果。针对以上两个问题,JMM给出happens-before通用的规则

为了保证java内存模型中的操作顺序,JMM为程序中的所有操作定义了一个顺序关系,这个顺序叫做Happens-Before。要想保证操作B看到操作A的结果,不管AB是在同一线程还是不同线程,那么AB必须满足Happens-Before的关系。如果两个操作不满足happens-before的关系,那么JVM可以对他们任意重排序。

两个操作间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行。happens-before仅仅要求前一个操作对后一个操作可见。

volatile 就是一个践行happens-before的关键字。happens-before指的是线程接收其他线程修改共享变量的消息与该线程读取共享变量的先后关系。volatile变量规则:对一个volatile的写,happens-before于任意后续对这个volatile变量的读。

Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序:源代码 —— 编译器优化重排 —— 指令级并行的重排序 —— 内存系统的重排序 —— 最终执行的指令序列
【1】编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
【2】指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
【3】内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

二、happens-before的规则

【1】程序顺序规则: 如果在程序中操作A在操作B之前,那么在同一个线程中操作A将会在操作B之前执行。这里的操作A在操作B之前执行是指在单线程环境中,虽然虚拟机会对相应的指令进行重排序,但是最终的执行结果跟按照代码顺序执行是一样的。虚拟机只会对不存在依赖的代码进行重排序。
【2】监视器锁规则: 监视器上的解锁操作必须在同一个监视器上面的加锁操作之前执行。如果线程1解锁了monitor a,接着线程2锁定了a,那么,线程1解锁a之前的写操作都对线程2可见(线程1和线程2可以是同一个线程)
【3】volatile变量规则:volatile变量的写入操作必须在对该变量的读操作之前执行。原子变量和volatile变量在读写操作上面有着相同的语义。如果线程1写入了volatile变量v(临界资源),接着线程2读取了v,那么,线程1写入v及之前的写操作都对线程2可见
【4】线程启动规则: 线程上对Thread.start的操作必须要在该线程中执行任何操作之前执行。假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行前对线程B可见。注意:线程B启动之后,线程A在对变量修改线程B未必可见。
【5】线程结束规则: 线程中的任何操作都必须在其他线程检测到该线程结束之前执行。线程t1写入的所有变量,在任意其它线程t2调用t1.join(),或者t1.isAlive()成功返回后,都对t2可见。
【6】中断规则: 当一个线程再另一个线程上调用interrupt时,必须在被中断线程检测到interrupt调用之前执行。
【7】终结器规则: 对象的构造函数必须在启动该对象的终结器之前执行完毕。对象调用finalize()方法时,对象初始化完成的任意操作,同步到全部主存同步到全部cache
【8】传递性: 如果操作A在操作B之前执行,并且操作B在操作C之前执行,那么操作A必须在操作C之前执行。

案例: 单例模式

public class Flight {private static Flight flight;public static Flight getFlight(){if(flight == null) {flight = new Flight();}return flight;}
}

上面的类中定义了一个getFlight方法来返回一个新的Flight对象,返回对象之前,我们先判断了flight是否为空,如果不为空的话就new一个Flight对象。但是如果考虑到JMM的重排规则,就会发现问题。flight = new Flight()其实一个复杂的命令,并不是原子性操作。它大概可以分解为**1.分配内存,2.实例化对象,3.将对象和内存地址建立关联。**其中2和3有可能会被重排序,然后就有可能出现book返回了,但是还没有初始化完毕的情况。从而出现不可以预见的错误。根据上面的happens-before规则,最简单的办法就是给方法前面加上synchronized关键字:

public class Flight {private volatile static Flight flight;public static Flight getFlight(){if(flight == null ){synchronized (Flight.class){if(flight == null) {flight = new Flight();}}}return flight;}
}

上面的类中检测了两次Flight的值,只有flight为空的时候才进行加锁操作。这里flight一定要是volatile。因为flight的赋值操作和返回操作并没有happens-before,所以可能会出现获取到一个仅部分构造的实例。这也是为什么我们要加上volatile关键词。

三、as-if-serial语义

as-if-serial语义: 不管怎么重排序,单线程程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。所以编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

::: warning
在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果。
在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。
:::

本质上来说Happens-before关系和as-if-serial语义是一回事,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。只不过后者只能作用在单线程,而前者可以作用在正确同步的多线程环境下:
as-if-serial语义保证单线程内程序的执行结果不被改变,Happens-before关系保证正确同步的多线程程序的执行结果不被改变。
as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。Happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按Happens-before指定的顺序来执行的。

四、案例

class VolitileExample {int a = 0;volatile boolean flag = false;public void reader() {if (flag == true) {int i = a;}}public void writer() {a = 10;flag = true;}
}

假设Thread A执行writer()方法之后,Thread B执行reader()方法。根据根据程序次序规则:1 Happens-before 23 Happens-before 4。根据volatile变量规则:2 Happens-before 3。根据传递性规则:1 Happens-before 31 Happens-before 4。也就是说,如果Thread B读到了flag==true或者int i = a那么Thread A设置的a=42Thread B是可见的。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Pytorch 复习总结 2

Pytorch 复习总结,仅供笔者使用,参考教材: 《动手学深度学习》Stanford University: Practical Machine Learning 本文主要内容为:Pytorch 线性神经网络。 本文以机器学习中的两大基本问题 —— 回归和分类为例,介绍…

【数据结构初阶 6】二叉树:堆的基本操作 + 堆排序的实现

文章目录 🌈 Ⅰ 二叉树的顺序结构🌈 Ⅱ 堆的概念与性质🌈 Ⅲ 堆的基本操作01. 堆的定义02. 初始化堆03. 堆的销毁04. 堆的插入05. 向上调整堆06. 堆的创建07. 获取堆顶数据08. 堆的删除09. 向下调整堆10. 判断堆空 🌈 Ⅳ 堆的基本…

【selenium】八大元素定位方式|xpath css id name...

目录 一、基础元素定位 二、cssSelector元素定位——通过元素属性定位 三、xpath元素定位——通过路径 1 、xpath绝对定位 (用的不多) 缺点:一旦页面结构发生变化(比如重新设计时,路径少两节)&#x…

从计网的角度讲明白什么是网关

网关(Gateway)又称网间连接器、协议转换器。网关在传输层上以实现网络互连,是最复杂的网络互连设备,仅用于两个高层协议不同的网络互连。网关的结构也和路由器类似,不同的是互连层。网关既可以用于广域网互连,也可以用于局域网互连…

JDBC实现连接池

简单上手 使用 JDBC 来执行 SQL 查询和更新操作 import java.sql.*;public class Main {public static void main(String[] args) {String url "jdbc:mysql://localhost:3306/your_database_name";String username "your_username";String password &…

vs2015零基础编译zlib从失败到成功

本博文源于笔者不断尝试从0编译zlib到能够将文件打包成zip的一个简单过程,其中包含了如何下载zlib,如何编译zlib,如何使用zlib等。内容堪比教科书级别,可以收藏,可以自用,希望博文能帮助到读者。 1、下载z…

Protocol Buffers v21.12 安装 ( linux 系统 )

下载 Protocol Buffers v21.12 Protocol Buffers v21.12 解压 tar zxvf protobuf-cpp-3.21.12.tar.gz执行 进入解压目录,执行下面configure可执行程序,目的是监测安装环境,生成makefile ./configure执行完后可以检查是否生成makefile文件 构…

Unity单元测试

Unity单元测试是一个专门用于嵌入式单元测试的库, 现在简单讲下移植以及代码结构. 源码地址: GitHub - ThrowTheSwitch/Unity: Simple Unit Testing for C 1.我们只需要移植三个文件即可: unity.c, unity.h, unity_internals.h 2.然后添加需要测试的函数. 3.在main.c中添加…

Swift基础知识:23.Swift析构过程

在 Swift 中,析构过程是指在一个类的实例被释放之前进行的清理操作。通常,这些清理操作包括释放实例所持有的资源、取消订阅通知、关闭文件等。Swift 使用析构器(deinitializer)来实现析构过程,它类似于构造器&#xf…

公司文件防泄密管理系统

公司文件防泄密管理系统是一种综合性的解决方案,旨在确保企业文件的安全性和保密性,防止内部员工或外部攻击者非法获取、泄露或篡改敏感信息。 PC端:https://isite.baidu.com/site/wjz012xr/2eae091d-1b97-4276-90bc-6757c5dfedee 以下是关于…

【JavaEE】_form表单构造HTTP请求

目录 1. form表单的格式 1.1 form表单的常用属性 1.2 form表单的常用搭配标签:input 2. form表单构造GET请求实例 3. form表单构造POST请求实例 4. form表单构造法的缺陷 对于客户端浏览器,以下操作即构造了HTTP请求: 1. 直接在浏览器…

IDM下载器2024中文版主要功能、使用场景、优点、缺点介绍

软件分析师眼中的IDM(Internet Download Manager) IDM绿色下载如下: https://wm.makeding.com/iclk/?zoneid34275 一、主要功能 高速下载:利用多线程技术和文件分块下载策略,显著提高下载速度。断点续传:即使在下载…

【Linux】MySQL数据库的使用

【Linux】MySQL数据库的使用 一、访问MySQL数据库二、创建及删除库和表1、创建新的库2、创建新的表3、删除一个数据表4、删除一个数据库 三、管理表中的数据记录1、插入数据记录2、查询数据记录3、修改数据记录4、删除数据记录 四、数据库用户授权1、授予权限2、查看权限3、撤销…

db-gpt docker部署进坑

1.docker 不支持gpu,参考之前文章 2.docker 容器启动就关闭,docker ps -a 查看容器状态为exited 停止 定位问题: (1) 查看日志 docker logs dbgpt -f (2) 日志错误最后一行: V…

《图解HTTP》笔记1:http的诞生

1,http的诞生: 1.1 为共享知识而生 我们现在使用web(World Wide Web的简称,即万维网)浏览器,目前可以输入一个网址(http://www.baidu.com),就会有一个网页显示出来。 最开始设想出…

jenkins远程触发构建报:Error 403 No valid crumb was included in the request

最近在跨jenkins触发构建的时候发现不能触发相应的项目,报如下图错误 解决方案: 1、安装Build Authorization Token Root Plugin插件 安装完成后去配置API Token,用户列表,配置用户的API Token,生成后记得保存 2、项…

YOLOv9来啦!YOLO目标检测全新工作!性能表现SOTA!在各个方面都大大超过了RT-DETR、YOLOv8等

摘要 今天的深度学习方法侧重于如何设计最合适的目标函数,使模型的预测结果最接近实际情况。同时,必须设计一种适当的架构,该架构可以促进获取足够的信息用于预测。现有的方法忽略了一个事实,即当输入数据经过逐层特征提取和空间…

NXP实战笔记(五):S32K3xx基于RTD-SDK在S32DS上配置ADC的硬件触发同步采样与软件采样过程

目录 1、概述 1.1、软件触发 1.2、硬件触发 - BCTU 1.3、硬件触发 - TRGMUX 1.4、ADC的校准 1.5、ADC时钟配置 2、BTCU硬件触发ADC的SDK配置 3、软件触发ADC 3.1、选择相应Port作为ADC的输入 3.2、ADC配置 3.3、代码示例 1、概述 恩智浦 S32K3xx 系列汽车微控制器…

【Linux】再谈进程地址空间

目录 一、引入 二、物理内存和外设空间的交互 三、解决页表过大问题 一、引入 我们在往期的博客中有讲解过进程地址空间:【Linux】进程地址空间 但是在上述博客中我们只是对进程地址空间的左边部分详细进行了讲解,下面我们就来谈谈右边的部分&#…

第3.6章:StarRocks数据导入——DataX StarRocksWriter

一、Datax 1.1 DataX 3.0概述 DataX3.0是一个异构数据源离线同步工具,可以方便的对各种异构数据源进行高效的数据同步。 其github地址为: https://github.com/alibaba/DataX/blob/master/introduction.mdhttps://github.com/alibaba/DataX/blob/mast…