20.有序性与内存屏障

文章目录

  • 有序性与内存屏障
  • 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进行重排序

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

混合组网VS传统网络:智能硬件混合组网优劣势浅要解析

智能硬件混合组网是一种利用多种通信技术相结合的方法&#xff0c;以实现更灵活、更可靠的网络连接。通过蓝牙、Wi-Fi、LoRa、4G相互之间的不同通讯方式&#xff0c;根据应用场景的不同以及现场实际环境&#xff0c;优选最佳物联网混合组网方案&#xff0c;以达到部署最便捷性价…

云曦2024年春季学期期中考复现

目录 Web Web_SINGIN 简简单单的文件上传 好玩的PHP 渗透的本质 简简单单的sql re baby_re easy xor Crypto easy_rsa Rsa2 Crypto_Singin Pwn pwn_Sing Misc easy_singin Xjpg 流量分析1 流量分析3 流量分析2 Web Web_SINGIN 1.使用右键检查&#xff0c…

IMU内参标定(理论)

1、内参标定标定什么&#xff1f; 生产零偏、标度因数误差、安装误差 2、现象是什么&#xff1f; 零偏现象&#xff1a;即使没有任何运动或旋转&#xff0c;IMU传感器仍然会输出一个非零的信号。零偏是一个恒定的误差&#xff0c;导致测量值始终偏离实际值。对于加速度计&am…

DolphinDB 携手九鞅科技,助力固收投研效能飞跃

随着金融市场开放的广度与深度不断拓宽&#xff0c;金融产品呈现出多样化的发展态势&#xff0c;其中债券投资组合凭借其低风险性、高流动性与稳健的收益表现&#xff0c;逐渐成为投资理财领域备受瞩目的焦点。投资经理不仅需要了解哪些债券值得投资&#xff0c;更要对债券投资…

【GESP试卷】2024年03月Scratch四级试卷

2024年GESP03月认证Scratch四级试卷 分数&#xff1a;100 题数&#xff1a;27 一、单选题(共15题&#xff0c;每题2分&#xff0c;共30分) 010203040506070809101112131415CDBBACBCDCDADBA 1、小杨的父母最近刚刚给他买了一块华为手表&#xff0c;他说手表上跑的是鸿蒙&…

朋友正确交往方式,以及保留有效沟通,才是对朋友的尊重!

人生就像一列火车&#xff0c;从生命之初驶向生命的终点&#xff0c;路途上有很多站点&#xff0c;每一个站点都会遇到不同的人&#xff0c;结交各式各样的朋友&#xff0c;中间有人下车&#xff0c;有人上车&#xff0c;有人与你走着走着就散了&#xff0c;有人偶有相见却已是…

Qt 科目一考试系统(有源码)

项目源码和资源&#xff1a;科目一考试系统: qt实现科目一考试系统 一.项目概述 该项目是一个基于Qt框架开发的在线考试系统&#xff0c;主要实现了考试题目的随机抽取、考试时间限制、成绩统计等功能。用户可以通过界面操作进行考试&#xff0c;并查看自己的考试成绩。 二.技…

计算机网络之应用层知识点总结

6.1 网络应用模型 &#xff08;1&#xff09;应用层概述 &#xff08;2&#xff09;网络应用模型的介绍 客户/服务器&#xff08;C/S&#xff09;模型 P2P模型 6.2 域名解析系统DNS &#xff08;1&#xff09;DNS系统介绍 &#xff08;2&#xff09;域名 &#xff08;3&#…

AI爆文写作:标题需要什么?情绪炸裂,态度要激烈,行为要夸张!

现在这个传播环境下&#xff0c;在公域中&#xff0c;轻声细语&#xff0c;慢慢的说&#xff0c;无法吸引到注意&#xff0c;没有人搭理。 标题要需要情绪张扬&#xff0c;态度激烈&#xff0c;行为夸张&#xff0c;大声喧闹。 唐韧的用户群是互联网产品经理&#xff0c;阅读量…

小猫咪的奇幻冒险:一个简单的Python小游戏

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、游戏简介与演示 二、游戏开发与运行 1. 环境搭建 2. 代码解析 3. 加速机制 三、游戏…

油猴插件刷学习通

油猴插件刷学习通 edge浏览器 浏览器进入这个网址https://microsoftedge.microsoft.com/addons/search/%E6%B2%B9%E7%8C%B4tampermonkey?hlzh-CN。 点我自动进入 点那个绿色的&#xff0c;点击获取 油猴插件下载在了这里 找到油猴图标&#xff0c;获取新脚本。 安装 …

DPDK实践之(1)dpdk基础使用

DPDK实践之(1)dpdk基础使用 Author: Once Day Date: 2024年5月19日 一位热衷于Linux学习和开发的菜鸟&#xff0c;试图谱写一场冒险之旅&#xff0c;也许终点只是一场白日梦… 漫漫长路&#xff0c;有人对你微笑过嘛… 全系列文档可参考专栏&#xff1a;Linux基础知识_Once…

Unity Terrain Adjust插件使用教程

一、Terrain Adjust插件介绍 二、插件下载以及导入 1、官方下载地址&#xff1a;Terrain Adjust 2、积分下载地址&#xff1a;Terrain Adjust 下载好之后&#xff0c;回到Unity当中&#xff0c;导入下载好之后的unitypackage包 三、插件使用 1、在使用之前一定要在场景中新…

【数据结构】二叉树的功能实现

文章目录 关于二叉树的创建如何创建二叉树实现二叉树的前、中、后序遍历层序遍历 关于二叉树的创建 在笔者的上一篇文章中堆进行了一个详细介绍&#xff0c;而二叉树是以堆为基础进行创建&#xff0c;它与堆的显著不同是 堆像是一个线性结构&#xff0c;堆的结构往往是一个数…

刷题之寻找重复数(leetcode)

寻找重复数 这题实际上就是变形的环形链表Ⅱ&#xff0c;下标为index的下一个元素是nums[index]&#xff0c;下下一个元素是nums[nums[index]] class Solution { public:int findDuplicate(vector<int>& nums) {int fast0;int slow0;while(1){fastnums[nums[fast]]…

力扣第141题和142题-环形链表,是否有环,环的入口节点

因这2道题均不改变链表结构&#xff0c;所以可以不创建新的临时头结点 /*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/ bool hasCycle(struct ListNode *head) {if(headNULL||head->nextNULL)//若只有一个数…

Linux笔记之命令行JSON处理器jq

Linux笔记之命令行JSON处理器jq code review! 文章目录 Linux笔记之命令行JSON处理器jq1.安装2.jq 基本用法3.例程3.1. 示例JSON文件3.2. 读取特定字段3.3. 管道过滤器&#xff08;Pipe Filters&#xff09;3.4. 映射过滤器&#xff08;Map Filters&#xff09;3.5. 条件过滤…

go 微服务框架kratos错误处理的使用方法及原理探究

通过go语言原生http中响应错误的实现方法&#xff0c;逐步了解和使用微服务框架 kratos 的错误处理方式&#xff0c;以及探究其实现原理。 一、go原生http响应错误信息的处理方法 处理方法&#xff1a; ①定义返回错误信息的结构体 ErrorResponse // 定义http返回错误信息的…

从零起航,Python编程全攻略

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、Python入门之旅 二、Python进阶之道 三、Python爬虫实战 四、Python数据分析利器 五…

STL--set和multiset集合

set和multiset会根据特定的排序准则&#xff0c;自动将元素排序。两者不同之处在于multiset 允许元素重复而 set 不允许。如下图: 使用set或multiset&#xff0c;必须先包含头文件: #include <set>上述两个类型都被定义为命名空间std内的class template: namespace std…