21.Happens-Before原则

2024-05-27 09:20
文章标签 21 原则 happens

本文主要是介绍21.Happens-Before原则,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 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; // 操作Ay = 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;      // 代码1flag = true;  // 代码2}public  synchronized void add() {if (flag) {  // 代码3value = x + x;System.out.println(Thread.currentThread().getName() + "- value:" + value);}}@Benchmarkpublic 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 而不是 200VolatileRecorderDemo 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;      // 代码1flag = true;  // 代码2}public  synchronized void add() {if (flag) {  // 代码3value = x + x; // 代码4System.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;  // 代码1flag = true;  // 代码2}public synchronized void add() {if (flag) {  // 代码3value = x + x; // 代码4System.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;// 启动线程BthreadB.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(() -> {// 启动线程BthreadB.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;}
}

在这里插入图片描述

这篇关于21.Happens-Before原则的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

【LabVIEW学习篇 - 21】:DLL与API的调用

文章目录 DLL与API调用DLLAPIDLL的调用 DLL与API调用 LabVIEW虽然已经足够强大,但不同的语言在不同领域都有着自己的优势,为了强强联合,LabVIEW提供了强大的外部程序接口能力,包括DLL、CIN(C语言接口)、ActiveX、.NET、MATLAB等等。通过DLL可以使用户很方便地调用C、C++、C#、VB等编程语言写的程序以及windows自带的大

JVM内存调优原则及几种JVM内存调优方法

JVM内存调优原则及几种JVM内存调优方法 1、堆大小设置。 2、回收器选择。   1、在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因为GC过后这个值是不会变化的,因此内存调优的时候要更多地使用JDK提供的内存查看工具,比如JConsole和Java VisualVM。   2、对JVM内存的系统级的调优主要的目的是减少

【JavaScript】LeetCode:21-25

文章目录 21 最大子数组和22 合并区间23 轮转数组24 除自身以外数组的乘积25 缺失的第一个正数 21 最大子数组和 贪心 / 动态规划贪心:连续和(count)< 0时,放弃当前起点的连续和,将下一个数作为新起点,这里提供使用贪心算法解决本题的代码。动态规划:dp[i]:以nums[i]为结尾的最长连续子序列(子数组)和。 dp[i] = max(dp[i - 1]

react笔记 8-21 约束性 表单

1、约束性组件和非约束性组件 非约束性组件<input type="text" name="" defaultValue={this.state.msg}></input>这里他的value是用户输入的值 并没有执行操作 只是获取到了msg的值 用户输入不会改变数据非约束性组件需要使用defaultValue获取数据 否则会报错约束性组件<input type="text

水处理过滤器运行特性及选择原则浅谈

过滤属于流体的净化过程中不可缺的处理环节,主要用于去除流体中的颗粒物或其他悬浮物。水处理过滤器的原理是利用有孔介质,从流体中去除污染物,使流体达到所需的洁净度水平。         水处理过滤器的滤壁是有一定厚度的,也就是说过滤器材具有深度,以“弯曲通 道”的形式对去除污染物起到了辅助作用。过滤器是除去液体中少量固体颗粒的设备,当流体进入置有一定规格滤网的滤筒后,其杂质被阻挡,而

内存管理篇-21 虚拟内存管理:线性映射区

1.线性映射区的定义         这部分讲线性映射区的内容。一般老的嵌入式平台,它内存很小只有几百兆,都会直接把整个物理内存映射到线性映射区了,只有当物理内存大于1GB以上,线性映射区无法cover的时候就把剩下的放到高端内存。所以这个区域是最简单的。         线性映射区一般是指内核空间的某个部分,直接映射到低端内存的区域。并且他们之间是线性映射的。         PAGE_O

重写equals和hashCode的原则规范

当符合以下条件时不需要重写equals方法:     1.     一个类的每一个实例本质上都是唯一的。     2.     不关心一个类是否提供了“逻辑相等”的测试功能     3.     超类已经改写了equals方法,并且从超类继承过来的行为对于子类也是合适的。     4.     一个类时私有的或者是package私有的,并且可以确定它的equals方法永远不会被调用。(这

职场关系课:职场上的基本原则(安全原则、进步原则、收益原则、逃生舱原则)

文章目录 引言安全原则进步原则收益原则逃生舱原则 引言 职场上的王者,身体里都应该有三个灵魂: 一个文臣,谨小慎微,考虑风险; 一个武将,积极努力,谋求胜利; 一个商人,精打细算,心中有数。 安全原则 工作安全:保住自己的工作和位置信用安全:保住个人的信用,如果领导看到了你的信用受损,你和领导的关系可能会持续恶化。人身安全:有的时候你会遇到偏执的人,要及时和

leetcode解题思路分析(三)15-21题

三数求和 最简单的做法就是三重循环判断: int length=nums.length;for(int i=0;i<length;i++){for(int j=i+1;j<length;j++){for(int k=j+1;k<length;k++){if(nums(i)+nums[j]+nums[k]==0){...}}} } 在此基础上,对第三次查找其实可以做优化:

浅谈数据库、JVM、缓存、SQL等性能调优方法和原则

浅谈数据库、JVM、缓存、SQL等性能调优方法和原则 java互联网架构 2019-07-07 13:19:00 性能优化基本是BAT等一线互联网公司程序员必备的技能,以下为大家完整揭晓性能完整的优化方案和方法:包含web网站调优、数据库、JVM调优、架构调优等方案。 第一:Web网站调优 1、尽可能减少HTTP请求:图片合并 (css sprites),Js脚本文件合并、css文件