20.有序性与内存屏障

news2024/10/5 15:32:00

文章目录

  • 有序性与内存屏障
  • 1.重排序
    • 1.1.编译器重排序
    • 1.2.CPU重排序
      • 1.2.1.指令级重排序
      • 1.2.2.内存系统重排序
      • 1.3.As-if-Serial规则
  • 2.内存屏障
    • 2.1.硬件层面的内存屏障
      • 2.1.2.写屏障
      • 2.1.3.读屏障
      • 2.1.4.全屏障
    • 2.2.硬件层的内存屏障作用
      • 2.3.案例

有序性与内存屏障

有序性 与 可见性 是两个完全不同的概念,虽然两者都是CPU不断升级迭代的产物,但是由于CPU的技术不断发展,为了重复释放硬件的高性能,编译器、CPU会优化待执行的指令序列,包括调整某些执行的执行顺序。优化的结果, 指令执行顺序会与代码顺序有所不同,可能导致代码出现有序性问题。

内存屏障又称为 内存栅栏,是一系列CPU指令,他的主要作用是保证特定操作的执行顺序,保证并发行的有序性。

在编译器 和 CPU都进行指令的重排序优化时,可以通过在指令间插入一个内存屏障指令,高速编译器 和 CPU,进制在内存屏障指令(前后)执行重排序。

重排序主要时分为两类

  • 编译器重排序
  • CPU重排序

在这里插入图片描述

1.重排序

1.1.编译器重排序

编译器重排序是指,在代码编译的阶段进行指令重排,不改变程序执行结果的情况下为了提升效率,编译器对指令进行乱序(Out-of-Order)的编译。

例如,在代码中,A操作需要获取其他资源进入等待状态,而B操作 和 A操作并没有数据依赖关系,如果编译器一直等待A操作执行的话,效率会慢很多,此时就可以先编译B的代码,这样的乱序可以提升编译速度。

编译器进行重排序(Re-Order)目的:与其等待阻塞指令,不如先去执行其他指令,和CPU的乱序相比编译器重排序能够更大范围的,效果更好的乱序优化。

1.2.CPU重排序

流水线(Pipline)和乱序执行(Out-of-Order Execution)是现代CPU基本具有的特性,及机器指令在流水线经理过 取指令、译码、执行、写回等操作。为了CPU的执行效率,流水线都是并行处理的,在不影响语义的情况下,处理器次序(机器指令在CPU实际执行的顺序)和程序次序(程序代码的执行顺序)是允许不一致的,只要满足As-if-Serial规则即可。(但是不影响语义只能把保证指令之间的显示关系,并不能保证程序之间的隐式关系)

其实乱序,实际上也遵循一定的规则:只要两个指令之间不存在【数据依赖】,就可以对这两个指令乱序。

1.2.1.指令级重排序

在不影响程序执行结果的情况下,CPU内核采用了ILP(指令级并行运算)技术将多条指令重叠执行,如果指令之间不存在数据依赖,那么处理器可以改变语句对应机器指令的执行顺序。

1.2.2.内存系统重排序

对于现在CPU来说,在CPU内核 和 主存之间都具备一个高速缓存,高速缓存主要为了减少CPU内核 和主存之间的交互,在CPU内核进行读操作时,也是先从高速缓存中读取,在CPU内核写的时候,也是先写入高速缓存,最后统一写入主存。无论是读还是写,都优先考虑高速缓存。

在内存系统重排序中,会有类似的优化措施,但是这些措施更多地涉及到如何合理地利用缓存、预取数据等方面,而不是对指令执行顺序的显式调整。

所以内存重排序 和 指令重排序不同,他时一种伪重排序,看起来像乱序执行而已。

1.3.As-if-Serial规则

在单核CPU场景下,当指令被重排序后,如何保证运行的一个正确性呢?其实也很简单,编译器 和CPU都需要遵守As-if-Serial原则。

As-if-Serial规则具体内容为:不管如何重排序,都必须保证代码在单线程下运行正确。

为了遵守As-if-Serial规则,编译器和CPU不会对存在数据依赖关系的操作进行重排序,因为这种重排序回改变执行结果,但是如果指令之间不存在数据依赖关系。这些指令可能会被编译器和CPU进行重排序。

public class AsIfSerialRuleDemo {
    public static void main(String[] args) {
        int a = 1;     // a
        int b = 2;     // b
        int c = a + b; // c
    }
}

例如上面这段代码, c 和 a之间存在依赖关系,c 和 b之间也存在依赖关系,因此在最终需要执行的指令序列中,c 不能重排序到 a 和 b的前面,因为 c 和 a,b有数据依赖,会导致程序执行的不正确。

但是 a 和 b没有依赖关系,编译器 和 CPU可以重排序 a 和 b的一个执行顺序

为了保证 As-if-Serial的规则,Java异常处理机制也会为指令重排序做一些特殊处理

public class AsIfSerialRuleDemo {
    public static void main(String[] args) {
        int x,y;
        x = 1;
        try {
            x = 2;
            y = 0 / 0;

        }catch (Exception e) {
            throw new RuntimeException("处理数据发生异常",e);
        }finally {
            System.out.println("x = " + x);
        }
    }
}

在上面这段代码中,语句 x = 2y= 0 /0之间没有数据依赖关系 所以 y = 0 / 0可能会被重排序在 x = 2之前执行,重排序后,x = 2并未能执行,但是y = 0 /0 已经抛出异常,那么最终的结果 x = 1这显然是不对的。

所以为了保证,最终不输出 x = 1的错误结果,JIT会在重排序时,会在catch语句中插入错误补偿代码,补偿执行语句 x = 2, 将程序恢复到发生异常时应有的状态,这样做法的确将异常的捕获和底层逻辑变得非常复杂,但是JIT的原则就是,尽力保证正确运行逻辑,哪怕以catch块的逻辑变得非常复杂也要保证。

但是,As-if-Serial规则只能保证单内核指令重排序之后的执行结果正确,不能保证多内核以及跨CPU指令重排序之后的执行结果正确

2.内存屏障

2.1.硬件层面的内存屏障

多核情况下,所有CPU操作都会涉及缓存一致性协议(MESI协议)校验,该协议用于保障内存可见性,但是缓存一致性协议仅仅保证内存弱可见(高速缓存失效),没有保证共享变量的强可见,而且缓存一致性更不能禁止CPU重排序,也就是不能保证跨CPU指令的有序执行。

如果保证CPU执行重排序之后的程序结果正确呢?需要使用到内存屏障

内存屏障又称为内存栅栏,是让一个CPU高速缓存的内存状态对其他CPU内核可见的一种技术,也是一项跨CPU保证有序性执行指令的技术。、

硬件层常用的内存屏障分为三种:写屏障,读屏障,全屏障

2.1.2.写屏障

在指令后插入写屏障指令能够将寄存器、高速缓存中的最新数据更新到主存,让其他线程可见,并且写屏障会告诉CPU和编译器,在写屏障之前的写指令必须要先于写屏障执行,不能进行指令重排序。

写屏障的作用是确保内存写操作的顺序性和可见性。在多线程并发编程中,当一个线程对共享变量进行写操作时,其他线程需要能够及时看到这些写操作的结果,而不是在缓存中读取到旧的数值。因此,插入写屏障指令可以保证这一点。

写屏障的插入不仅会将数据同步到主存,还会阻止编译器和CPU对写操作进行重排序。这样做的目的是为了避免在多线程环境下出现不一致的情况,例如数据竞争或者并发执行时的意外结果。

总之,指令后面插入写屏障指令,有两个作用:

  1. 能让寄存器、告诉缓存中最新的数据写回到主内存。
  2. 在写屏障之前的写指令,必须先于写屏障执行,不能进行指令重排。

2.1.3.读屏障

读屏障是将高速缓存中相应的数据失效,在指令前插入读屏障,可以让高速缓存中的数据失效,强制重新从主存中加载数据,并且读屏障会告诉CPU和编译器,后于这个屏障的读指令必须后执行,不能对后面的读操作进行指令重排

总之,在指令前插入的读屏障指令,有两个作用:

  1. 让高速缓存中的数据失效,从主存中加载数据
  2. 后于读屏障的读指令必须后执行,不能对后面的读操作进行指令重排

2.1.4.全屏障

全屏障的作用是确保在多线程环境下,对共享变量的读写操作符合程序员的预期。它可以防止指令重排序和缓存一致性等问题带来的可见性和有序性问题,从而提供了较强的线程间通信和同步保证。

在Java中,全屏障的使用可以通过volatile关键字、synchronized关键字、Lock接口等方式实现。这些机制都能够保证全屏障的效果,使得多线程程序能够正确地共享数据和进行同步操作。

全屏障具有以下特性:

  1. 在全屏障之前的指令必须在全屏障之前执行完成,全屏障之后的指令必须在全屏障之后执行。
  2. 在全屏障之前的读操作必须在全屏障之前完成,全屏障之后的读操作必须在全屏障之后完成。
  3. 在全屏障之前的写操作必须在全屏障之前完成,全屏障之后的写操作必须在全屏障之后完成。
  4. 全屏障会使得所有处理器的缓存无效,强制从主内存中重新加载数据。

2.2.硬件层的内存屏障作用

  1. 阻止屏障两侧的指令重排序
    • 编译器和CPU可能为让性能得到优化而进行指令重排,但是插入一个硬件层的内存屏障相当于告诉CPU和编译器先于这个屏障的指令必须先执行,后于这个屏障的指令必须后执行
  2. 强制把新数据写回主存,并让高速缓存的数据失效
    • 硬件层的内存屏障强制把高速缓存中的最新数据写回主内存,让高速缓存中的相应脏数据失效,一旦写入完成,然和访问这个变量的线程都将会得到最新的值。

2.3.案例

package com.hrfan.java_se_base.base.thread.cas.rule;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
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 java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Slf4j
public class OutOfOrderExample {
    public int x = 0;
    public Boolean flag = false;


    public void update() throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            x = 8;  // 语句 1
            new BigDecimal("3.121654894").multiply(new BigDecimal("1565.45664")).multiply(new BigDecimal("5656.565666")).add(new BigDecimal("56464660.66")).divide(new BigDecimal("1"));
            flag = true; // 语句 2
        }
    }

    public void show() {
        if (flag) {
            log.error(" x = {}", x);
            // 输出完成 重新恢复 x = 0,不然每次输出的都是 8
        }
    }


}

class TestThread {


    @Test
    public void test() {
        OutOfOrderExample outOfOrderExample = new OutOfOrderExample();
        startTestThread(outOfOrderExample);
    }


    private static void startTestThread(OutOfOrderExample outOfOrderExample) {
        CountDownLatch latch = new CountDownLatch(10);
        List<Thread> threads = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(() -> {
                for (int j = 0; j < 2000; j++) {
                    try {
                        outOfOrderExample.update();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    outOfOrderExample.show();
                }
                latch.countDown();
            }));
        }


        // 启动全部线程
        threads.forEach(Thread::start);


        // 等待全部线程执行完毕
        try {
            latch.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

在这里插入图片描述

OutOfOrderExample并发之后之后,控制台的x的值控可能为0 或者 8,为什么 x会输出0 呢,主要原因是 update()show()方法可能在两个CPU内核之间并发执行,如果 语句1语句2 发生了重排序,那么show()方法可能输出的就是 0

那么如何保证并发运行结果的正确呢?

Java语言没有办法直接使用硬件层的内存屏障,只能使用含有JMM内存屏障语义的的Java关键字,我们直接使用volatile关键字 修饰变量在来观察结果。

    public volatile int x = 0;
    public volatile Boolean flag = false;

在这里插入图片描述

修改后的 OutOfOrderExample 代码使用关键字volatile关键字对成员变量进行修饰,volatile含有JMM全屏障的语义,要求JVM编译器在语句1前后插入全屏障指令,该全屏障确保x的最新值对所有的后续操作是可见的(含跨CPU场景),并且禁止编译器 和CPU对语句1 和 语句2进行重排序

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

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

相关文章

基于英飞凌BGT60LTR11AIP E6327芯片具低功耗的脉冲多普勒操作模式常用于汽车应用的雷达上

芯片特征&#xff1a; 60 GHz收发器MMIC&#xff0c;带一个发射器和一个接收器单元封装天线&#xff08;AIP&#xff09;&#xff08;6.73.30.56 mm3)低功耗的脉冲多普勒操作模式自主模式用于运动和运动方向的集成检测器运动检测信号的直接输出目标检测范围的15个可配置阈值检测…

汇编-16位汇编环境搭建

16位汇编环境 在学习16位汇编时&#xff0c;我选择的环境是在VMware中安装Windows XP虚拟机来学习&#xff1b;因为Windows XP提供了兼容的DOS环境&#xff0c;可以直接运行和调试16位汇编程序&#xff1b;在win10&#xff0c;win11环境中原生不支持直接运行 16 位程序&#x…

华为鸿蒙认证培训 | 讯方技术成为首批鸿蒙原生应用开发及培训服务商

5月20日&#xff0c;鸿蒙原生应用合作交流推介会-深圳站在深圳中洲万豪酒店隆重举行。讯方技术作为鸿蒙钻石服务商受邀参与此次活动&#xff0c;活动由讯方技术总裁刘国锋、执行副总裁刘铭皓、教学资源部部长张俊豪共同出席。 本次活动由深圳政府指导&#xff0c;鸿蒙生态官方…

微服务项目收获和总结---第4天(文章审核和保存)

文章审核以及APP端保存文章 业务流程&#xff1a; App端保存接口&#xff1a; 数据库表详情 文章的基本信息表&#xff1a;id&#xff0c;标题&#xff0c;作者id&#xff0c;频道id...... 文章的权限/配置表&#xff1a;存储文章是否可以评论&#xff0c;是否上架&#xff…

eclipse配置JDK和Tomcat

eclipse配置JDK jdk配置 配置JDK&#xff1a; 首先&#xff0c;确保JDK已经安装并配置了环境变量。这包括设置JAVA_HOME环境变量&#xff0c;指向JDK的安装目录&#xff0c;以及更新CLASSPATH和PATH环境变量以包含JDK的bin目录。 在Eclipse中&#xff0c;通过Window > Pre…

【设计模式深度剖析】【4】【创建型】【建造者模式】| 类比选购汽车的过程,加深理解

&#x1f448;️上一篇:抽象工厂模式 | 下一篇:原型模式&#x1f449;️ 目录 建造者模式概览定义英文原话直译如何理解呢&#xff1f;建造者模式将对象的建造过程给抽象出来了类比选购汽车 4个角色UML类图1. 抽象建造者&#xff08;Builder&#xff09;角色2. 具体建造者…

02.爬虫---HTTP基本原理

02.HTTP基本原理 1.URI 和 URL 的区别2.HTTP 和 HTTPS 的区别3.请求过程 1.URI 和 URL 的区别 URL&#xff08;Uniform Resource Locator&#xff09;即-统一资源定位符 URL是用来定位和访问互联网上资源的独特标识&#xff0c;它包括了资源的位置&#xff08;如IP地址或域名&a…

Docker-制作镜像的两种方式=》基于容器制作基于Dockerfile制作

本文主要是基于Docker如何制作一个Java镜像&#xff0c;而一个Java镜像想要运行需要系统环境&#xff0c;JDK。所以我们要先有一个系统环境&#xff0c;本文使用的是centos7&#xff0c;JDK选择版本是8&#xff0c;而我使用的Java程序是一个简易的springBoot项目&#xff0c;你…

详细分析Vue按钮间距的优化方式(附Demo)

目录 前言1. 按钮间距2. 垂直间距 前言 正常间距如下&#xff1a; 其Demo如下&#xff1a; <el-table-column label"操作" align"center"><template #default"scope"><el-buttonlinktype"primary"click"openFor…

Node.js —— 前后端的身份认证 之用 express 实现 JWT 身份认证

JWT的认识 什么是 JWT JWT&#xff08;英文全称&#xff1a;JSON Web Token&#xff09;是目前最流行的跨域认证解决方案。 JWT 的工作原理 总结&#xff1a;用户的信息通过 Token 字符串的形式&#xff0c;保存在客户端浏览器中。服务器通过还原 Token 字符串的形式来认证用…

Mysql注入详细讲解

特殊字符 0x3a:0x7e~0x23# 注入基础 联合查询注入(union) :::tips 页面将SQL查询内容显示出来&#xff0c;即为有回显&#xff0c;可以尝试联合查询注入 利用关键字union &#xff0c;union all 拼接恶意SQL语句 ::: 注入流程 有报错&#xff0c;可以利用报错。如&#xff…

[datawhale202405]从零手搓大模型实战:TinyAgent

结论速递 TinyAgent项目实现了一个简单的Agent智能体&#xff0c;主要是实现了ReAct策略&#xff08;推理调用工具的能力&#xff09;&#xff0c;及封装了一个Tool。 项目实现有一定的疏漏。为了正确运行代码&#xff0c;本次对代码Agent部分进行了简单修改&#xff08;完善…

VBA语言専攻每周通知20240524

通知20240524 各位学员∶本周MF系列VBA技术资料增加611-615讲&#xff0c;T3学员看到通知后请免费领取,领取时间5月24日晚上18:00-5月26日晚上18:00。本次增加内容&#xff1a; MF611:用InputBox录入日期 MF612:信息提示10秒后关自动关闭 MF613:只是信息提示10秒 MF614:显…

Zynq-Linux移植学习笔记之68- 国产ZYNQ添加用户自定义版本信息

1、背景介绍 在使用复旦微zynq时&#xff0c;有时候虽然针对uboot源码进行了改动&#xff0c;但由于uboot基线版本只有一个&#xff08;2018-07-fmsh&#xff09;&#xff0c;导致无法区分版本信息&#xff0c;虽然可以通过编译时间来区分&#xff0c;但没有版本号直观。内核也…

【Numpy】深入解析numpy中的ravel方法

NumPy中的ravel方法&#xff1a;一维化数组的艺术 &#x1f308; 欢迎莅临我的个人主页&#x1f448;这里是我深耕Python编程、机器学习和自然语言处理&#xff08;NLP&#xff09;领域&#xff0c;并乐于分享知识与经验的小天地&#xff01;&#x1f387; &#x1f393; 博主简…

香港服务器负载过高的原因和应对办法

保持网站正常运行看似简单&#xff0c;但事实上&#xff0c;有许多问题会影响网站和应用程序的性能&#xff0c;并可能导致停机。其中一个问题就是服务器过载。而香港服务器作为一种常见的服务器类型&#xff0c;有时会出现负载过高的情况。为了帮助您确保在香港服务器过载不会…

AI应用案例:电能量异常分析智能诊断系统

窃电和计量装置故障造成漏收、少收电费使电力系统利益受损。一般情况主要通过定期巡检、定期校验电表、用户举报窃电等手段来发现窃电或计量装置故障。对人的依赖性太强&#xff0c;抓窃查漏的目标不明确。利用电力系统中逐步积累下来的海量真实数据&#xff0c;采用数据挖掘技…

C++多生产者,多消费者模型

C11实现多生产者&#xff0c;多消费者模型 在C标准库中实现多生产者多消费者模型&#xff0c;可以使用std::thread、std::queue、互斥锁(std::mutex)、条件变量(std::condition_variable)等组件。下面是一个简单的示例&#xff0c;展示如何创建多生产者和多消费者模型&#xf…

构建智能化的语言培训教育技术架构:挑战与机遇

随着全球化的发展和人们对语言学习需求的增长&#xff0c;语言培训教育行业正面临着越来越多的挑战和机遇。在这个背景下&#xff0c;构建智能化的语言培训教育技术架构成为提升服务质量和效率的重要手段。本文将探讨语言培训教育行业的技术架构设计与实践。 一、智能化教学平台…

Jupyter Notebook的三个使用场景:网页端、PyCharm专业版和VScode

说明&#xff0c;以下都是我个人的摸索感悟和总结&#xff0c;自己理解和猜测的是这样&#xff0c;欢迎指正。 Jupyter Notebook的三个常用使用地方&#xff08;网页端、PyCharm专业版、VScode&#xff09;&#xff1a; 总结一句话&#xff1a;网页端、PyCharm中和VScode中三…