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

news2025/2/1 17:45:36

优质博文: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.coloradmin.cn/o/1093730.html

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

相关文章

docker保存镜像出错

报错:open .docker_temp_801673807: Access is denied. 查询后发现是因为C盘权限问题导致失败,修改保存路径:docker save -o D:\nginx.tar nginx:latest后成功。

解决docker使用pandarallel报错OSError: [Errno 28] No space left on device

参考:https://github.com/nalepae/pandarallel/issues/127 在使用pandarallel报错OSError: [Errno 28] No space left on device,根据上述issue发现确实默认使用的MEMORY_FS_ROOT为 /dev/shm,而在docker环境下这个目录大小只有64M&#xff0…

​嵌入式VS纯软件

嵌入式VS纯软件 嵌入式系统开发与纯软件开发之间存在关键差异,涵盖了硬件依赖性、资源限制、实时性要求、安全性和维护等多个方面。最近很多小伙伴找我,说想要一些嵌入式资料,然后我根据自己从业十年经验,熬夜搞了几个通宵&#x…

序列中排列存在类dp问题+结合组合数学和拆贡献:1014T4

http://47.92.197.167:5283/contest/412/problem/4 赛时就想到枚举开头来拆贡献。 先说一下,对于A我们不关心具体的值,我们只关心哪些位置相等,哪些位置不等,最后乘上一个系数就行 然后对于序列是否存在排列类问题有个常见的dp套…

【API篇】二、源算子API

文章目录 0、demo数据1、源算子Source2、从集合中读取数据3、从文件中读取4、从Socket读取5、从Kafka读取6、从数据生成器读取数据7、Flink支持的数据类型8、Flink的类型提示(Type Hints) 0、demo数据 准备一个实体类WaterSensor: Data All…

导航栏参考代码

导航栏参考代码 <!DOCTYPE html> <html> <head> <meta charset"utf-8"> <title>导航栏参考代码</title> </head> <body> <table width"858" border"0" align"center"><tr&g…

ASO优化之应用评分评论对APP下载量增长的重要性

APP应用评分和评论对于下载转化率的效果是显著的&#xff0c;是ASO优化重要的因素之一。应用评分是对应用性能的衡量预判&#xff0c;而应用评论是用户向应用提供的反馈总结。 1、评分的重要性。 应用评分显示在应用商店的搜索结果&#xff0c;特色页面&#xff0c;热门图表中…

第五十七章 学习常用技能 - 查看Globals

文章目录 第五十七章 学习常用技能 - 查看Globals查看Globals测试查询并查看查询计划 第五十七章 学习常用技能 - 查看Globals 查看Globals 要查看一般Globals&#xff0c;可以使用 ObjectScript ZWRITE 命令或管理门户中的全局页面。如果正在寻找存储类数据的Globals&#x…

docker搭建nginx+php-fpm

docker run --name nginx -p 8898:80 -d nginx:1.20.2-alpine# 将容器nginx.conf文件复制到宿主机 docker cp nginx:/etc/nginx/nginx.conf /usr/local/nginx/conf/nginx.conf# 将容器conf.d文件夹下内容复制到宿主机 docker cp nginx:/etc/nginx/conf.d /usr/local/nginx/conf…

Linemod算法研究

转载&#xff0c;这篇博客写的比较详细&#xff0c;分析也到位. https://www.cnblogs.com/aoru45/p/16810996.html

Linux查看端口号及进程信息

Linux查看端口号及进程 Linux查看端口号 netstat netstat -tuln显示当前正在监听的端口号以及相关的进程信息 ss ss -tuln与netstat类似&#xff0c;ss也可以用于显示当前监听的端口以及相关信息 isof isof -i :端口号端口号替换为具体要查找的端口号&#xff0c;显示该端…

Mysql数据库 4.图形化界面工具DataGrip

DataGrip工具 选择进入官网安装 ataGrip: The Cross-Platform IDE for Databases && SQL by JetBrains 下载最新版本的可以直接点击 Download 下载&#xff0c;下载其他版本的点击 Other versions 下载其他版本 .选择对应的版本进行下载即可 下载完&#xff0c;得到…

Redis中的BigKey如何发现和处理

文章目录 什么是BigKey?大键的存在通常被认为是不好的,主要原因:常见的bigkey原因: BigKey危害&#xff1f;占用大量内存空间阻塞服务器进程加长持久化时间延长复制时间增加内存碎片加重AOF重写压力降低查找效率 如何发现BigKey&#xff1f;info命令scan命令Redis-cli第三方工…

002数据安全传输-多端协议传输平台:配置Oracle数据库-19c及导入数据信息

002多端协议传输平台&#xff1a;配置Oracle数据库-19c及导入数据信息 文章目录 002多端协议传输平台&#xff1a;配置Oracle数据库-19c及导入数据信息1. 数据库准备2. 导入sql脚本2.1 原版Oracle-11g脚本2.2 新版Oracle-19c脚本2.3 命令行导入脚本 3. 删除系统中数据库信息sql…

在雷电模拟器9上安装magisk并安装LSPosed模块以及其Manager管理器(二)之LSPosed的使用

上一篇已经安装好LSPosed模块及其Manager管理器&#xff0c;参考文章 在雷电模拟器9上安装magisk并安装LSPosed模块以及其Manager管理器&#xff08;一&#xff09;-CSDN博客 安装完成后&#xff0c;在模拟器上出现图标如下&#xff1a; 一、运行LSPosed 二、仓库模块 内容非…

《向量数据库指南》——Milvus Cloud和Elastic Cloud 特性对比

随着以 Milvus 为代表的向量数据库在 AI 产业界越来越受欢迎,诸如 Elasticsearch 之类的传统数据库和检索系统也开始行动起来,纷纷在快速集成专门的向量检索插件方面展开角逐。 例如,在提供类似插件的传统数据库中,Elasticsearch 8.0 首屈一指,推出了包括向量插入和最相似…

蓝桥杯 第 1 场算法双周赛 第1题 三带一 c++ map 巧解 加注释

题目 三带一【算法赛】https://www.lanqiao.cn/problems/5127/learning/?contest_id144 问题描述 小蓝和小桥玩斗地主&#xff0c;小蓝只剩四张牌了&#xff0c;他想知道是否是“三带一”牌型。 所谓“三带一”牌型&#xff0c;即四张手牌中&#xff0c;有三张牌一样&#…

字符串进行 URL 编码处理

// URL_test.cpp : Defines the entry point for the console application. //#include "stdafx.h"#include <stdio.h> #include <stdlib.h> #include <string.h>// 判断字符是否需要进行 URL 编码 static bool needs_encoding(char c) {if ((c &g…

力扣第538题 把二叉搜索树转换为累加树 c++

题目 538. 把二叉搜索树转换为累加树 中等 相关标签 树 深度优先搜索 二叉搜索树 二叉树 给出二叉 搜索 树的根节点&#xff0c;该树的节点值各不相同&#xff0c;请你将其转换为累加树&#xff08;Greater Sum Tree&#xff09;&#xff0c;使每个节点 node 的新值…

相似性搜索:第 3 部分--混合倒排文件索引和产品量化

接续前文&#xff1a;相似性搜索&#xff1a;第 2 部分&#xff1a;产品量化 SImilarity 搜索是一个问题&#xff0c;给定一个查询的目标是在所有数据库文档中找到与其最相似的文档。 一、介绍 在数据科学中&#xff0c;相似性搜索经常出现在NLP领域&#xff0c;搜索引擎或推…