21.Happens-Before原则

news2024/11/14 19:38:20

文章目录

  • Happens-Before原则
    • 1.Happens-Before规则介绍
    • 2.规格介绍
      • 2.1.顺序性规则(as-if-serial)
      • 2.2.volatile规则
      • 2.3.传递性规则
      • 2.4.监视锁规则
      • 2.5.start规则
      • 2.6.join()规则

Happens-Before原则

JVM内存屏障指令对Java开发工程师是透明的,是JMM对JVM实现的一种规范和要求。

JMM定义了一套自己的规则,Happens-Before规则(先行发生),并且确保两个Java语句必须存在Happens-Before关系,JMM尽量确保这俩个Java语句之间的内存可见性和指令的有序性。

1.Happens-Before规则介绍

  1. 程序顺序执行规则(as-if-serial规则)
    1. 在同一个线程中有依赖关系的操作规则按照先后顺序,前一个操作必须先行发生于后面一个操作(例如线程 A - 线程 B 有依赖关系,线程A必须先执行,然后才能执行线程B)。
    2. 总的来说就是单线程顺序无论怎么排序,对于结果来是不会变得。
  2. volatile变量规则
    1. 对于volatile修饰的变量,必须的写操作,必须先于发生对volatile的读操作
  3. 传递性规则
    1. 如果线程 A 先行发生于线程 B 操作,线程 B 操作又先行发生于 线程 C 操作,那么线程 A 必须先行发生于线程 C操作。
  4. 监视锁规则(Monitor Lock Rule)
    1. 对于一个监视锁的解锁操作先行发生 于 后续对这个监视锁的加锁操作。(同一个线程,解锁操作,必须先于加锁操作之前执行)
  5. start规则
    1. 对于线程的start操作先行发生于这个线程内部的其他任何操作。具体来说,如果线程A执行线程B的start()方法,那么线程A 启动线程 B 的 start()方法,先于发生线程B中的任意操作
  6. join规则
    1. 如果线程A执行了 线程B的join()方法,那么线程B的任意操作,先行发生于线程A所执行的 线程B的join()方法

2.规格介绍

2.1.顺序性规则(as-if-serial)

顺序性规则的具体内容:一个线程内,按照代码顺序书写在前面的操作,先行发生于 书写在后面的操作。

顺序性规则是Java内存模型(JMM)中一个基本的概念,它保证了在一个线程内部观察到的操作执行顺序,会符合程序代码的逻辑顺序。这意味着,在单线程环境下,程序的执行将保持我们书写的指令顺序,不会出现乱序执行的情况。简单来说,如果在代码中先写了操作A,然后是操作B,那么在同一个线程中执行时,A必然会在B之前完成,不会出现B先于A执行的结果。

虽然可能发生重排序,但是他只对不存在数据依赖代码行,进行指令重排序

int x = 0;
int y = 0;

void method() {
    x = 1; // 操作A
    y = 2; // 操作B
}

根据顺序性规则,在method方法内部,操作A(x = 1;)总是会先于操作B(y = 2;)执行完成。这意味着,在同一个线程调用method方法后,任何检查xy值的后续代码都将看到x为1且y为2,不可能看到y已经更新为2而x还未被设置为1的情况。

2.2.volatile规则

volatile的具体内容:对一个volatile变量的的写,先行发生于任意后续对这个volatile变量的读。

volatile的主要作用:

  1. **保证可见性:**对一个volatile变量的修改,能够立即刷新到主内存中,所有其他线程对该变量的访问都会重新从主内存中获取最新值,从而确保了变量的可见性。
  2. **禁止指令重排序:**volatile除了保证变量的可见性外,还有一层重要的意义在于它能禁止指令的重排序优化。具体来说,对volatile变量的写操作,在写后,会有一个内存屏障(Memory Barrier),确保该写操作不会被重排序到之后的读写操作之前;相应的,对volatile变量的读操作前也会有一个内存屏障,确保该读操作不会被重排序到之前的写操作之后。这正是你提到的“对一个volatile变量的写,先行发生于任意后续对这个volatile变量的读。
package com.hrfan.thread;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

 // 每个参与测试的线程都将拥有其独立的实例
 @State(Scope.Thread)
public class VolatileRecorderDemo {


     int x = 10;
     int value = 0;
     boolean flag = false;

    public  void update() {
        x = 100;      // 代码1
        flag = true;  // 代码2
    }

    public  synchronized void add() {
        if (flag) {  // 代码3
            value = x + x;
            System.out.println(Thread.currentThread().getName() + "- value:" + value);
        }
    }



    @Benchmark
    public void test(){
        // 假设线程1 执行 update(),线程2 执行 add(),代码1 和 代码2 并没有依赖关系,所以 代码1 和 代码2 可能被重排序,他们排序后结果为
        // flag = true;
        // value = 100;
        // 假设重排序后,线程1 执行了 flag = true; 此时还没有执行  value = 10; 线程2 开始执行 add()方法.此时value的值为 10
        // 那么最终结果 value = 20 而不是 200
        VolatileRecorderDemo volatileRecorderDemo = new VolatileRecorderDemo();
        volatileRecorderDemo.update();
        volatileRecorderDemo.add();

    }


    @Test
    @DisplayName("测试")
    public void start() {
        Options opt = new OptionsBuilder()
                .include(VolatileRecorderDemo.class.getSimpleName())
                // 预热3轮
                .warmupIterations(3)
                // 度量5轮
                .measurementIterations(5)
                // 设置线程数,比如设置为4个线程
                .threads(200)
                // fork的JVM实例数量 每轮任务数量
                .forks(5)
                .build();

        try {
            new Runner(opt).run();
        } catch (RunnerException e) {
            throw new RuntimeException(e);
        }
    }
}

假设线程A执行 update()方法,线程B 执行 add()方法,因为代码1 和代码2并没有依赖关系,所以代码1 和代码2就可能会被重排序,他们重排序后的次序可能为

flag = true;  // 代码2
value = 100;      // 代码1

线程A执行重排代码后,在完成 代码2 之前(flag = true),假设线程B开始执行 add()方法,将 x 的值进行累加,此时的 x 的值就是 10 而不是100,那么 x 累加完成后的值就是 20。这个不是我们想要的结果,为了获取正确的结果,我们必须阻止代码进行重排序,为以上代码的flag成员属性增加 volatile修饰,

public class VolatileRecorderDemo {


     int x = 10; 
     int value = 0;
     volatile boolean flag = false;

    public  void update() {
        x = 100;      // 代码1
        flag = true;  // 代码2
    }

    public  synchronized void add() {
        if (flag) {  // 代码3
            value = x + x; // 代码4
            System.out.println(Thread.currentThread().getName() + "- value:" + value);
        }
    }
}

从前面的顺序性规则,已经知道,如果 代码2的操作为 volatile写,无论第一个操作是什么都不能重排序。所以代码1 不会排到 代码2 后面的。

代码3 为读取 flag(volatile)变量,那么 代码4 就不会被重排序到 代码3 之前。

2.3.传递性规则

如果线程 A 先行发生于线程 B 操作,线程 B 操作又先行发生于 线程 C 操作,那么线程 A 必须先行发生于线程 C操作。

例如,如果线程A修改了一个变量,然后线程B读取了这个变量的值,并且线程B接着修改了另一个变量,线程C随后读取线程B修改的变量值,根据传递性规则,线程A对第一个变量的修改操作在逻辑上必须在C线程读取第二个变量值之前发生,保证了跨线程间操作的正确序列化。

2.4.监视锁规则

对于一个监视锁的解锁操作先行发生 于 后续对这个监视锁的加锁操作。(同一个线程,解锁操作,必须先于加锁操作之前执行)

在Java内存模型中,监视锁规则规定了对于同一个监视器(Monitor,通常指由synchronized关键字实现的同步块或方法)的解锁操作,必须先行发生于后续针对该监视器的加锁操作。这意味着,如果线程A解锁了一个监视器(即退出了同步代码块或方法),那么这个解锁操作将发生在任何其他线程B(包括线程A自身)随后成功获取这个监视器锁的操作之前。这一规则确保了以下几点:

  • 线程间的操作顺序性:锁的解锁和加锁操作为线程间的操作提供了一种全局的顺序关系,帮助维护操作的执行顺序性,这对于理解并发程序的行为至关重要。
  • 内存可见性:解锁操作之前的所有内存写操作对随后的加锁操作后的线程都是可见的,确保了数据的正确同步。
  • 互斥性:保证了在任何时刻只有一个线程可以持有监视器的锁,从而防止数据竞争条件和不一致状态的读取。
public class VolatileRecorderDemo2 {


    int x = 10;
    int value = 0;
    boolean flag = false;

    public synchronized void update() {
        value = 100;  // 代码1
        flag = true;  // 代码2
    }

    public synchronized void add() {
        if (flag) {  // 代码3
            value = x + x; // 代码4
            System.out.println("x = " + x);
        }
    }


}

先获取锁的线程,读 value 赋值之后,释放锁,那么另外一个线程 再去获取锁的时候,一定能看到对 value赋值的改动。

在这里插入图片描述

2.5.start规则

对于线程的start操作先行发生于这个线程内部的其他任何操作。具体来说,如果线程A执行线程B的start()方法,那么线程A 启动线程 B 的 start()方法,先于发生线程B中的任意操作

简单来说,就是 主线程A 启动 子线程B 后,线程B 能看到 线程A启动操作前的任何操作

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class StartRuleDemo {
    private static final Logger log = LoggerFactory.getLogger(StartRuleDemo.class);
    private int x = 0;
    private int y = 0;
    private Boolean flag = false;


    @Test
    @DisplayName("测试start()规则")
    public void testStartRule() throws InterruptedException {
        // 创建线程B 先不启动
        Thread threadB = new Thread(this::printInfo, "线程B");
        // 线程A 先 对数据进行赋值操作
        Thread threadA = new Thread(() -> {
            x = 100;
            y = 200;
            flag = true;

            // 启动线程B
            threadB.start();

        }, "线程A");


        threadA.start();


    }


    public void printInfo() {
        log.error("============================ 线程B打印相关信息 ============================");
        log.error("x = {}", x);
        log.error("y = {}", y);
        log.error("flag = {}", flag);
    }
}

在这里插入图片描述

2.6.join()规则

join()规则的具体内容是:如果线程A 执行 threadB.join() 操作后,并成功返回。那么线程B中的任意操作先行发生于线程A的 threadB.Join()

这意味着,通过调用join(),线程A确保了线程B的所有操作都已经完成了,这包括线程B的执行、修改共享变量、资源释放等,所有这些都完全发生在线程A得以继续其后续代码执行之前。这保证了线程间操作的顺序性和数据的一致性,避免了因并发执行可能产生的数据竞争问题。

join() 规则 刚好和 start()规则 相反

在Java中,Thread.join()方法是一个非常重要的同步机制,它允许一个线程等待另一个线程执行完成后再继续执行。

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class StartRuleDemo {
    private static final Logger log = LoggerFactory.getLogger(StartRuleDemo.class);
    private int x = 0;
    private int y = 0;
    private Boolean flag = false;

    @Test
    @DisplayName("测试join()规则")
    public void testJoinRule() throws InterruptedException {
        // 创建线程B 先不启动
        Thread threadB = new Thread(this::updateThreadB, "线程B");
        // 线程A 先 对数据进行赋值操作
        Thread threadA = new Thread(() -> {
            // 启动线程B
            threadB.start();
            try {
                threadB.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            printInfo();

        }, "线程A");


        threadA.start();


    }


    public void printInfo() {
        log.error("============================ 线程A打印相关信息 ============================");
        log.error("x = {}", x);
        log.error("y = {}", y);
        log.error("flag = {}", flag);
    }


    public void updateThreadB(){
        this.x = 100;
        this.y = 200;
        this.flag = true;
    }
}

在这里插入图片描述

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

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

相关文章

基于51单片机温度报警系统—数码管显示

基于51单片机温度报警系统 (仿真+程序+原理图+设计报告) 功能介绍 具体功能: 1.DS18B20采集温度,数码管显示温度; 2.温度测量范围:0-99度; 3.当温度低于…

Qt for android 获取USB设备列表(二)JNI方式 获取

简介 基于上篇 [Qt for android 获取USB设备列表(一)Java方式 获取], 这篇就纯粹多了, 直接将上篇代码转换成JNI方式即可。即所有的设备连接与上篇一致。 (https://listentome.blog.csdn.net/article/details/139205850) 关键代码…

FPGA实现多路并行dds

目录 基本原理 verilog代码 仿真结果​ 基本原理 多路并行dds,传统DDS的局限性在于输出频率有限。根据奈奎斯特采样定理,单路DDS的输出频率应小于系统时钟频率的一半。但是在很多地方,要使采样率保持一致,所以,为了…

逻辑这回事(一)----FPGA安全编码规范

安全编码的背景、定义 FPGA攻击方式和攻击目的 安全编码价值 2020年4月,来自德国的研究者披露了一个名为“StarBleed”的漏洞,当时引起了业内一片轰动。这种漏洞存在于赛灵思的Virtex、Kintex、Artix、Spartan 等全部7系列FPGA中。通过这个漏洞&#…

【JavaWeb】Day83.Maven高级——分模块设计与开发

分模块设计与开发 介绍 所谓分模块设计,顾名思义指的就是我们在设计一个 Java 项目的时候,将一个 Java 项目拆分成多个模块进行开发。 1). 未分模块设计的问题 如果项目不分模块,也就意味着所有的业务代码是不是都写在这一个 Java 项目当中…

C语言—深入理解指针(4)

1.回调函数 回调函数就是一个通过函数指针调用的函数。 如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,被调用的函数就是回调函数。回调函数不是由该函数的实现方直接调用&#xff0…

[Algorithm][动态规划][简单多状态DP问题][按摩师][打家劫舍Ⅱ][删除并获得点数][粉刷房子]详细讲解

目录 1.按摩师1.题目链接2.算法思路详解3.代码实现 2.打家劫舍 II1.题目链接2.算法思路详解3.代码实现 3.删除并获得点数1.题目链接2.算法思路详解3.代码实现 4.粉刷房子1.题目链接2.算法思路详解3.代码实现 1.按摩师 1.题目链接 按摩师 2.算法思路详解 思路: 确…

《计算机网络微课堂》3-10 以太网交换机的生成树协议 STP

我们介绍以太网交换机生成树协议的基本概念。 请大家思考一下,应该如何提高以太网的可靠性呢?例如如图所示的以太网,由三台交换机互联而成,每个交换机上都连接有一些主机,为了简单起见,我们只画出了每个交…

源码部署ELK

目录 资源列表 基础环境 关闭防护墙 关闭内核安全机制 修改主机名 添加hosts映射 一、部署elasticsearch 修改limit限制 部署elasticsearch 修改配置文件 单节点 集群(3台节点集群为例) 启动 二、部署logstash 部署logstash 添加配置文件 启动 三、部署kiban…

嵌入式全栈开发学习笔记---C语言笔试复习大全22

目录 结构体 结构体的声明 定义结构体变量 访问结构体成员进行初始化 通过结构体变量名访问结构体成员 结构体指针 结构体指针的定义 通过结构体指针访问结构体成员 结构体数组 结构体数组的定义 遍历结构体数组 结构体的长度(笔试重点) 上一…

在家庭影院音频中应用的D类音频放大器

家庭影院的主要组成部分包括显示设备、音响设备、信号源和接线设备等。家庭影院的音响信号需要进行处理和输出,以获得高质量的音效。音响设备通常需要一台功率适当的数字、模拟混合的处理器,对音源进行降噪、均衡、扩展等处理操作,以达到高品…

VMare下载安装

一.下载 1.百度搜索BROADCOM官网 打开官网: https://www.broadcom.com/​ 2.点击右上角,进行账号注册,注册好后,进行登陆 3.注册好后,进入个人界面:https://support.broadcom.com/#. 按下图所示点击进…

React 组件创建以及使用

1.创建和嵌套组件 React 应用程序是由 组件 组成的。一个组件是 UI(用户界面)的一部分,它拥有自己的逻辑和外观。组件可以小到一个按钮,也可以大到整个页面。 React 组件是返回标签的 JavaScript 函数: function My…

【计算机毕业设计】基于SSM+Vue的校园美食交流系统【源码+lw+部署文档】

目录 前 言 第1章 概述 1.1 研究背景 1.2 研究目的 1.3 研究内容 第二章 开发技术介绍 2.1 Java技术 2.2 Mysql数据库 2.3 B/S结构 2.4 SSM框架 第三章 系统分析 3.1 可行性分析 3.1.1 技术可行性 3.1.2 经济可行性 3.1.3 操作可行性 3.2 系统性能分析 3.3 系…

无线技术整合到主动噪声控制(ANC)增强噪声降低性能

主动噪声控制(ANC)已成为一种广泛使用的降噪技术。基本原理是通过产生与外界噪音相等的反向声波,将噪音中和,从而达到降噪的效果。ANC系统通常包括以下几个部分:参考麦克风、处理芯片、扬声器和误差麦克风。参考麦克风…

Drone+Gitee自动执行构建、测试和发布工作流

拉取Drone:(至于版本,你可以下载最新的) sudo docker pull drone/drone:2 拉取runner: sudo docker pull drone/drone-runner-docker 在Gitee中添加第三方应用: 进入个人主页,点击设置: 往下翻,找到数…

Web工程和Servlet

使用idea创建web项目 第一种方式:使用原型创建web项目, Archetype的选择如下图: 创建完成: 第二种方式:不使用原型创建web工程 点击new,选择tomcat的bin的上一级目录 创建完毕 使用: 再webapp目录下…

重新思考:Netflix 的边缘负载均衡

声明 本文是对Netflix 博客的翻译 前言 ​ 在先前关于Zuul 2开源的文章中,我们简要概述了近期在负载均衡方面的一些工作。在这篇文章中,我们将更详细地介绍这项工作的原因、方法和结果。 ​ 因此,我们开始从Zuul和其他团队那里学习&#…

【限免】杂波环境下线性调频脉冲、巴克码、频率步进脉冲雷达MTI、脉冲压缩【附MATLAB代码】

来源:微信公众号:EW Frontier 本代码主要模拟杂波环境(飞机、地杂波、鸟类信号)下,Chirp脉冲、巴克码脉冲、频率步进脉冲雷达信号的脉冲压缩及MTI、​匹配滤波。 MATLAB主代码 % 定义参数 fs 1000; % 采样率 T 1; …

C语言 | Leetcode C语言题解之第106题从中序与后序遍历序列构造二叉树

题目: 题解: int post_idx;typedef struct {int key;int val;UT_hash_handle hh; } hashTable;hashTable* idx_map;void insertHashTable(int x, int y) {hashTable* rec malloc(sizeof(hashTable));rec->key x;rec->val y;HASH_ADD_INT(idx_m…