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

2023-10-15 07:15

本文主要是介绍解密并发编程的时间之谜:揭开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是可见的。

这篇关于解密并发编程的时间之谜:揭开Happens-Before的神秘面纱的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/216186

相关文章

go中的时间处理过程

《go中的时间处理过程》:本文主要介绍go中的时间处理过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1 获取当前时间2 获取当前时间戳3 获取当前时间的字符串格式4 相互转化4.1 时间戳转时间字符串 (int64 > string)4.2 时间字符串转时间

SpringBoot中SM2公钥加密、私钥解密的实现示例详解

《SpringBoot中SM2公钥加密、私钥解密的实现示例详解》本文介绍了如何在SpringBoot项目中实现SM2公钥加密和私钥解密的功能,通过使用Hutool库和BouncyCastle依赖,简化... 目录一、前言1、加密信息(示例)2、加密结果(示例)二、实现代码1、yml文件配置2、创建SM2工具

Go语言数据库编程GORM 的基本使用详解

《Go语言数据库编程GORM的基本使用详解》GORM是Go语言流行的ORM框架,封装database/sql,支持自动迁移、关联、事务等,提供CRUD、条件查询、钩子函数、日志等功能,简化数据库操作... 目录一、安装与初始化1. 安装 GORM 及数据库驱动2. 建立数据库连接二、定义模型结构体三、自动迁

python实现对数据公钥加密与私钥解密

《python实现对数据公钥加密与私钥解密》这篇文章主要为大家详细介绍了如何使用python实现对数据公钥加密与私钥解密,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录公钥私钥的生成使用公钥加密使用私钥解密公钥私钥的生成这一部分,使用python生成公钥与私钥,然后保存在两个文

Golang如何对cron进行二次封装实现指定时间执行定时任务

《Golang如何对cron进行二次封装实现指定时间执行定时任务》:本文主要介绍Golang如何对cron进行二次封装实现指定时间执行定时任务问题,具有很好的参考价值,希望对大家有所帮助,如有错误... 目录背景cron库下载代码示例【1】结构体定义【2】定时任务开启【3】使用示例【4】控制台输出总结背景

C++ 函数 strftime 和时间格式示例详解

《C++函数strftime和时间格式示例详解》strftime是C/C++标准库中用于格式化日期和时间的函数,定义在ctime头文件中,它将tm结构体中的时间信息转换为指定格式的字符串,是处理... 目录C++ 函数 strftipythonme 详解一、函数原型二、功能描述三、格式字符串说明四、返回值五

从基础到进阶详解Pandas时间数据处理指南

《从基础到进阶详解Pandas时间数据处理指南》Pandas构建了完整的时间数据处理生态,核心由四个基础类构成,Timestamp,DatetimeIndex,Period和Timedelta,下面我... 目录1. 时间数据类型与基础操作1.1 核心时间对象体系1.2 时间数据生成技巧2. 时间索引与数据

python多线程并发测试过程

《python多线程并发测试过程》:本文主要介绍python多线程并发测试过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、并发与并行?二、同步与异步的概念?三、线程与进程的区别?需求1:多线程执行不同任务需求2:多线程执行相同任务总结一、并发与并行?1、

利用Python实现时间序列动量策略

《利用Python实现时间序列动量策略》时间序列动量策略作为量化交易领域中最为持久且被深入研究的策略类型之一,其核心理念相对简明:对于显示上升趋势的资产建立多头头寸,对于呈现下降趋势的资产建立空头头寸... 目录引言传统策略面临的风险管理挑战波动率调整机制:实现风险标准化策略实施的技术细节波动率调整的战略价

使用Java实现Navicat密码的加密与解密的代码解析

《使用Java实现Navicat密码的加密与解密的代码解析》:本文主要介绍使用Java实现Navicat密码的加密与解密,通过本文,我们了解了如何利用Java语言实现对Navicat保存的数据库密... 目录一、背景介绍二、环境准备三、代码解析四、核心代码展示五、总结在日常开发过程中,我们有时需要处理各种软