18.并发编程原子性、可见性、有序性原理

news2024/12/25 12:46:13

文章目录

  • 原子性、可见性、有序性原理
  • 1.CPU物理缓存结构
    • 1.1.寄存器
    • 1.2.缓存
    • 1.3.物理内存
  • 2.并发编程的三大问题
    • 2.1.原子性
    • 2.2.可见性
    • 2.3.有序性
      • 2.3.1.指令重排
    • 2.4.总结

原子性、可见性、有序性原理

原子性,可见性,有序性,是并发编程中所面临的三大问题,Java通过CAS操作已经解决了并发编程中的原子性问题,下面来看下Java如何解决另外两个问题,可见性和有序性

1.CPU物理缓存结构

由于CPU的运算速度比物理内存的存取速度快很多,为了提高处理速度,现在CPU不直接和主存进行通信,而是在CPU和主存之间设计了多层高速的Cache(高速缓存),越靠近CPU的缓存越快,容量越小

为了更好的理解,我们可以打开自己的电脑的任务管理器,可以看到其CPU型号,缓存大小,线程相关信息,如下
在这里插入图片描述

多核CPU的三层缓存,它的存储结构究竟是如何设计的呢?下面我们通过图先来了解一些相关概念

在这里插入图片描述

1.1.寄存器

寄存器是计算机内部的一种高速存储器,直接与 CPU 核心相连。它们通常以硬件寄存器的形式存在于 CPU 中,并用于暂时存储指令、数据和中间结果。寄存器的主要特点包括:

  1. 极快的访问速度: 寄存器位于 CPU 核心内部,与执行单元直接相连,因此具有非常高的访问速度。寄存器的访问时间通常在1-2个 CPU 周期左右,远远快于其他存储器。
  2. 容量有限: 由于寄存器是集成在 CPU 核心内部的,因此其容量相对有限。典型的 CPU 中包含的寄存器数量在几十个到几百个不等,只能存储少量的数据和中间结果。
  3. 用途多样: 寄存器可以存储指令的操作数、中间计算结果以及控制信息。它们在 CPU 的指令执行过程中起到了临时存储和传递数据的作用,支持了 CPU 的运算和控制功能。
  4. 作为指令执行的基础: CPU 在执行指令时需要使用寄存器存储指令的操作数和中间结果。寄存器的快速访问速度和直接与 CPU 核心相连的特性使得它们成为了指令执行的基础。

1.2.缓存

缓存是计算机系统中用于提高数据访问速度的一种高速存储器。它位于 CPU 和主内存之间,作为主内存和 CPU 之间的中间存储器,存储了最近被频繁访问的数据和指令。缓存的主要特点包括:

  1. 加速数据访问: 缓存通过存储最近被访问的数据和指令,减少了 CPU 对主内存的访问次数,从而提高了数据的访问速度和系统的性能。
  2. 分级结构: 缓存通常被划分为多级(例如 L1、L2 和 L3 缓存),每一级缓存的容量和访问速度不同。这种分级结构可以根据访问频率和访问延迟来优化数据存储和访问效率。
  3. 容量逐级增大: 缓存的容量随着级别的增加而逐级增大,L1 缓存容量最小,L3 缓存容量最大。这种设计可以根据数据访问模式和访问延迟来调整数据存储的层次结构。
  4. 速度逐级降低: 缓存的访问速度随着级别的增加而逐级降低,L1 缓存速度最快,L3 缓存速度最慢。这种设计可以根据访问频率和访问延迟来优化数据的存取速度。
  5. 数据共享: 在多核处理器中,L3 缓存通常被多个 CPU 核心共享,用于提高多个核心之间的数据共享和通信效率。

1.3.物理内存

物理内存是计算机系统中的主要存储介质,用于存储程序、数据和操作系统所需的信息。它是计算机系统中的一种硬件设备,通常被称为 RAM(随机存取存储器)。物理内存的主要特点包括:

  1. 存储容量: 物理内存的存储容量取决于计算机硬件的配置,通常以 GB(Gigabyte)为单位进行表示。现代计算机通常具有几 GB 到数十 GB 的物理内存容量。
  2. 数据访问速度: 物理内存的数据访问速度比较快,但远远慢于 CPU 内部的寄存器和缓存。物理内存的访问时间通常在数十到数百个 CPU 周期之间。
  3. 持久性: 物理内存是一种持久性存储介质,存储在其中的数据在断电后不会丢失,因此适合存储程序和数据。
  4. 地址空间: 物理内存通过地址线和数据线进行访问,每个内存单元都有一个唯一的地址。物理内存的地址空间取决于计算机的位数,32 位系统可以寻址的物理内存空间为 2^32 字节,即 4 GB;而 64 位系统可以寻址的物理内存空间则更大。
  5. 操作系统管理: 操作系统负责管理物理内存的分配和释放,以及内存的访问权限和保护。操作系统通过虚拟内存管理技术将物理内存抽象为虚拟内存,为每个进程提供了独立的内存空间,从而实现了内存的隔离和保护。

2.并发编程的三大问题

由于需要尽可能的释放CPU的能力,CPU上不断增加内核和缓存,内核也是越增加越多,缓存也层数也逐渐增加,就导致了并发编程中可见性 和有序性的问题。

2.1.原子性

所谓原子操作,就是一个不可中断的一个或者一系列操作,是不会被线程调度机制打段的一个操作。

我们来看一段代码

/**
 * 输出自增
 */
public class NumberAddTest {
    private static int sum = 0;
    public static void main(String[] args) {
        sum++;
    }
}

build项目 然后找到class文件,然后我们通过javap命令解析出 NumberAddTest的汇编代码

javap -c build/classes/java/main/com/hrfan/java_se_base/base/thread/volatile_new/NumberAddTest.class

编译出的汇编代码为

Compiled from "NumberAddTest.java"
public class com.hrfan.java_se_base.base.thread.volatile_new.NumberAddTest {
  public com.hrfan.java_se_base.base.thread.volatile_new.NumberAddTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #7                  // Field sum:I
       3: iconst_1
       4: iadd
       5: putstatic     #7                  // Field sum:I
       8: return

  static {};
    Code:
       0: iconst_0
       1: putstatic     #7                  // Field sum:I
       4: return
}
  1. 构造函数 com.hrfan.java_se_base.base.thread.volatile_new.NumberAddTest():
    • 这是类 com.hrfan.java_se_base.base.thread.volatile_new.NumberAddTest 的构造函数。
    • 代码行 0 加载了当前对象(this)。
    • 代码行 1 调用了父类 java.lang.Object 的构造函数。
    • 代码行 4 返回。
  2. 主函数 public static void main(java.lang.String[]):
    • 这是一个静态的 main 方法,是程序的入口点。
    • 代码行 0 获取静态字段 sum 的值。
    • 代码行 3 对 sum 的值加 1。
    • 代码行 5 将新的 sum 的值存储回静态字段 sum。
    • 代码行 8 返回。
  3. 静态初始化块 static {}:
    • 这是一个静态的初始化块,在类加载时执行。
    • 代码行 0 将值 0 存储到静态字段 sum。
    • 代码行 4 返回。

通过上面我可以看出,其实++ 实际上是3个操作

  1. 获取静态字段 sum 的值。
  2. 对 sum 的值加 1。
  3. 将新的 sum 的值存储回静态字段 sum(给sum重新赋值)。

这3个操作之间是可以发生线程切换的,或者说是可以被其他线程打断的,所以说++操作不是原子操作。

2.2.可见性

一个线程对共享变量的修改,另一个线程能够立刻看见,我们称之为该共享变量具备内存可见性

内存可见性的概念需要先了解 Java Memory Model (JMM)。JMM规定了多线程环境下内存操作的规则和行为,保证了多线程程序的正确性。

内存可见性是指一个线程对共享变量的修改能够被其他线程及时感知到。在多线程环境下,每个线程都有自己的工作内存,线程对共享变量的操作首先发生在工作内存中,然后通过主内存进行线程间的通信。因此,当一个线程修改了共享变量的值后,其他线程不一定能立即看到这个修改,这就是内存可见性的问题。

考虑两个线程 A 和 B 同时修改一个共享变量的情况,如果线程 A 修改了共享变量的值,但这个修改还没有被刷新到主内存,那么线程 B 可能无法立即看到这个修改。这种情况下,线程 B 可能会继续使用自己工作内存中的旧值,导致数据不一致的问题。

为了解决内存可见性的问题,可以通过以下方式确保共享变量的修改能够被其他线程及时感知到:

  1. 使用 volatile 关键字:将共享变量声明为 volatile 可以保证变量的修改对所有线程可见。当一个线程修改了 volatile 变量的值后,该值会立即被刷新到主内存,并且其他线程在访问该变量时会从主内存中获取最新的值。
  2. 使用锁:通过使用锁来实现线程间的同步,可以保证多个线程对共享变量的修改操作是互斥的,从而避免了多个线程同时修改共享变量导致的内存可见性问题。
  3. 使用原子类:Java 提供了一系列原子类(如 AtomicInteger、AtomicLong 等),这些类提供了一种线程安全的方式来进行原子操作,保证了操作的原子性和内存可见性。

注意:为什么Java局部变量,方法参数不存在内存可见性问题?

在Java中,所有局部变量(定义在栈中),方法定义参数不会在线程之间共享,所有也就不会存在内存可见性问题。

所有的Object实例,Class实例,数组元素都存储在JVM堆内存中,堆内存在线程之间共享,所以存在可见性问题。

在这里插入图片描述

线程A 可能不知道 这个值已经被线程A 修改了,就导致了可见性问题。

2.3.有序性

所谓程序的有序性,指的是程序执行顺序按照代码的先后顺序进行执行,如果程序执行的顺序和代码的先后顺序不同,那么就会导致错误结果,就发生给你了有序性问题。

下面我通过一个简单的案例来了解一下

@Slf4j
public class OrderTest {
    private static int x = 0;
    private static int y = 0;
    private static int a = 0;
    private static int b = 0;

    @Test
    @DisplayName("测试多线程环境下有序性问题")
    public void test(){
        Thread thread1 = new Thread(() -> {
            a = 1;
            x = b;
        });
        
        Thread thread2 = new Thread(() -> {
            b = 1;
            y = a;
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 输出 x、y 的值
        log.error("x = " + x + ", y = " + y);
    }
}

在这里插入图片描述

在这里插入图片描述

理想情况下,thread1 先执行,x 应该等于 b 的值,而 thread2 后执行,y 应该等于 a 的值。因此,xy 应该都等于 1。

但是由于 并发执行无序性的存在,可能会导致 thread2 先执行,然后 thread1 执行。这样一来,xy 的值可能都是 0,而不是我们期望的 1。

一次结果并不能说明问题,下面我们通过多次循环来观察结果

    @Test
    @DisplayName("测试多线程环境下有序性问题(循环)")
    public void test2(){

        while (true){
            x = 0 ;
            y = 0 ;
            a = 0 ;
            b = 0 ;
            Thread thread1 = new Thread(() -> {
                a = 1;
                x = b;
            });

            Thread thread2 = new Thread(() -> {
                b = 1;
                y = a;
            });

            thread1.start();
            thread2.start();

            try {
                thread1.join();
                thread2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 输出 x、y 的值
            log.error("x = " + x + ", y = " + y);
        }

    }

循环多次,发现了一个奇怪的数据,x = 0,y = 0,这种情况就说明发生了并发有序性问题

在这里插入图片描述

错误的执行顺序,导致了最终结果为 x = 0,y = 0

在这里插入图片描述

事实上出现了乱序结果,并不代表一定发生了指令重排序,内存可见性也有可能导致这样的输出,但是指令重排是导致出现乱序的原因之一

2.3.1.指令重排

指令重排序

指令重排序是现代计算机体系结构中的一种优化技术,用于提高程序执行的性能。它指的是编译器、处理器或者内存系统在不改变程序语义的前提下,重新安排指令的执行顺序。

为什么进行指令重排序?

指令重排序的目的是尽可能地提高 CPU 的利用率和整体性能。在现代计算机体系结构中,处理器具有多个执行单元和流水线,能够同时执行多条指令,并且存在各种优化技术(如超标量执行、乱序执行等),这些优化技术需要处理器在执行指令时能够灵活地重排序指令,以充分利用处理器资源,提高指令级并行性和流水线利用率。

指令重排序的类型

指令重排序主要分为编译器重排序、处理器重排序和内存系统重排序三种类型。

  • 编译器重排序: 编译器在生成目标代码时会对指令进行重排序,以优化程序的执行效率。例如,编译器可能会对代码进行指令调度和循环展开等优化操作。
  • 处理器重排序: 处理器在执行指令时会对指令进行重排序,以提高指令级并行性和流水线利用率。例如,处理器可能会对乱序执行的指令进行重新排序,以最大程度地减少流水线停顿。
  • 内存系统重排序: 内存系统在读写内存时会对指令进行重排序,以提高内存访问效率。例如,内存系统可能会对内存读写操作进行缓存重排等优化操作。

指令重排序的影响

指令重排序虽然能够提高程序执行的性能,但也可能会引入一些问题,主要包括:

  • 内存可见性问题: 指令重排序可能会导致共享变量的值对其他线程不可见,从而引发内存可见性问题。
  • 程序执行顺序错误: 指令重排序可能会改变程序中操作的执行顺序,导致程序的行为与预期不符。

为了避免指令重排序带来的问题,需要采取相应的措施来保证程序的正确性和一致性,如使用 volatile 关键字、同步机制等。

2.4.总结

总之,要保证并发程序能够正确的执行,必须保证 原子性,可见性,有序性,三者缺一不可!

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

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

相关文章

四轮麦轮平衡车四个轮子安放位置要求,以及编码器测速注意事项(强调,否则无法正常平移)——基于STM32F103ZET6

轮子推荐ABBA&#xff0c;当然BAAB也可以 如图安放&#xff1a; 这两种安防位置可以实现平移效果 若要实现平移则需要先实现PID控制平衡&#xff0c;这里用到520编码电机&#xff0c;相较于370电机他的动力更足&#xff0c;在调节PID时能节约不少时间而且更加容易。 需要注意…

AI应用案例:AI就是这么牛,聊天就能分析股票。

今天给大家介绍一个可能跟你切身相关的案例“大模型股票分析“&#xff01;首先申明这不是教你怎么炒股的方法&#xff0c;而是告诉你可以通过AI协助你去分析股票。 像我是程序员出身&#xff0c;有数据给我&#xff0c;我可以写SQL语句查数据库&#xff0c;或者通过写代码来分…

QT上位机开发

目录 前言一、环境搭建1.1 IDE下载1.2 添加环境变量 二、Qt Creator的使用2.1 快捷键2.2 创建QT项目2.3 帮助文档使用 三、Qt信号与槽3.1 标准信号与槽的使用3.2 自定义信号与槽 四、Qt控件4.1 QMainWindow4.2 按钮4.3 容器4.4 窗口4.5 布局管理 五、SerialPort5.1 修改.Pro文件…

【Python】 Python函数返回多个值的多种方法

基本原理 在Python中&#xff0c;函数通常用于封装一段代码&#xff0c;使其可以重复调用。有时&#xff0c;我们希望一个函数能够返回多个值&#xff0c;Python提供了几种不同的方法来实现这一点。 代码示例 示例1&#xff1a;使用元组返回多个值 Python中&#xff0c;元组…

TYPE-C接口桌面显示器:办公与娱乐新玩法

随着科技的不断进步&#xff0c;传统桌面显示器已经难以满足现代人对高效办公与极致娱乐体验的追求。在这个背景下&#xff0c;新型的TYPE-C接口桌面显示器应运而生&#xff0c;以其独特的功能和设计&#xff0c;引领了未来办公与娱乐的新潮流。 添加图片注释&#xff0c;不超过…

Linux 批量网络远程PXE

一、搭建PXE远程安装服务器 1、yum -y install tftp-server xinetd #安装tftp服务 2、修改vim /etc/xinetd.d/tftpTFTP服务的配置文件 systemctl start tftp systemctl start xinetd 3、yum -y install dhcp #---安装服务 cp /usr/share/doc/dhc…

云上聚智共创未来 | 移动云的项目实战,10分钟让你获得高度可玩的个人博客网站

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 引入 随着互联网的发展各种以前看起来离我们比较遥远的词越来越近了&#xff0c;比如 云服务、大数据、区块链、容器这些听起来…

免费且非常火的日程管理软件:飞项

一、简介 1、在日常繁忙的工签中&#xff0c;是否事情一大堆却记不住&#xff1f;系统自带的日历用着却是不方便&#xff0c;不顺手&#xff0c;提醒不及时&#xff1f;待办、打卡、记事乱七八糟的混在一起&#xff0c;关键时候找不到&#xff1f;市面上的日程管理软件那么多&a…

用go语言实现一个有界协程池

写在文章开头 本篇文章算是对go语言系列的一个收尾&#xff0c;通过go语言实现一个实现一个简单的有界协程池。 Hi&#xff0c;我是 sharkChili &#xff0c;是个不断在硬核技术上作死的 java coder &#xff0c;是 CSDN的博客专家 &#xff0c;也是开源项目 Java Guide 的维护…

【C语言】自定义类型:联合与枚举的简明概述

&#x1f525;引言 关于自定义类型除了我们常用的结构体&#xff0c;还有联合与枚举也是属于自定义类型。本篇将简单介绍联合与枚举基本概念和使用方法 &#x1f308;个人主页&#xff1a;是店小二呀 &#x1f308;C语言笔记专栏&#xff1a;C语言笔记 &#x1f308;C笔记专栏&…

中国改革报是什么级别的报刊?在哪些领域具有较高的影响力?

中国改革报是什么级别的报刊&#xff1f;在哪些领域具有较高的影响力&#xff1f; 《中国改革报》是国家发展和改革委员会主管的全国性综合类报纸。它在经济领域和改革发展方面具有重要的影响力&#xff0c;是传递国家政策、反映改革动态的重要平台。该报对于推动中国的经济改…

【Java限流算法详解及实现】

Java限流算法详解及实现 在高并发场景下&#xff0c;限流是保护系统的重要手段。限流算法可以帮助我们在流量过大时进行合理的控制&#xff0c;避免系统崩溃。本文将详细介绍几种常见的限流算法及其在Java中的实现。 Java限流算法详解及实现 Java限流算法详解及实现一、令牌桶…

Android 深入系统源码探讨 Activity、Window 和 View 的关系与实践

文章目录 1、概括2、Android Window 设计2.1、Window 类2.2、PhoneWindow2.3、WindowManager2.4、ViewRootImpl2.5、DecorView 3、Android Activity 设计3.1、Activity的基本概念3.2.、Activity的生命周期3.3、Activity的内部结构 4、Android View 设计4.1、View的基本概念4.2、…

构建智慧城市公共服务系统的功能架构设计

随着城市化进程的加速&#xff0c;城市公共服务系统在保障居民生活品质、提升城市管理水平方面扮演着愈发重要的角色。构建智慧城市公共服务系统的功能架构设计至关重要&#xff0c;它不仅需要充分考虑居民需求与城市管理的实际情况&#xff0c;还需要整合先进的科技手段&#…

vue3快速入门(局部使用)

目录 前置知识JavaScript-导入导出 入门操作 变量渲染页面 局部使用vue的实现步骤 vue指令 v-for v-bind v-if v-show v-on v-model 生命周期 前置知识JavaScript-导入导出 正常情况在html导入js文件是全部导入&#xff0c;这样会导致性能上的损失 。 JS提供的…

Docker之xfs文件系统下安装报错解决方案

一、需求说明 centos系统下安装docker最新版的时候&#xff0c;安装成功&#xff0c;启动的时候报错。报错信息“failed to start daemon: error initializing graphdriver: overlay2: the backing xfs filesystem is formatted without d_type support, which leads to incorr…

5.26机器人基础-DH参数 正解

1.建立DH坐标系 1.确定Zi轴&#xff08;关节轴&#xff09; 2.确定基础坐标系 3.确定Xi方向&#xff08;垂直于zi和zi1的平面&#xff09; 4.完全确定各个坐标系 例子&#xff1a; 坐标系的布局是由个人决定的&#xff0c;可以有不同的选择 标准坐标系布局&#xff1a; …

NDIS协议驱动(一)

Microsoft Windows 操作系统使用基于 1978 年国际标准化组织 (ISO) 开发的七层网络模型的网络体系结构。 ISO 开放系统互连 (OSI) 参考模型将网络描述为“一系列协议层&#xff0c;具有分配给每个层的一组特定函数。 每个层都向更高层提供特定的服务&#xff0c;同时阻止这些层…

vscode:如何解决”检测到include错误,请更新includePath“

vscode:如何解决”检测到include错误&#xff0c;请更新includePath“ 前言解决办法1 获取includePath路径2 将includePath路径添加到指定文件3 保存 前言 配置vscode是出现如下错误&#xff1a; 解决办法 1 获取includePath路径 通过cmd打开终端&#xff0c;输入如下指令&a…

有个小伙把 MyBatis 替换成 MyBatis-Plus,上线后就被开了!!

MyBatis-Plus 替换 MyBatis 首先&#xff0c;我们准备了一张名为 tbl_order 的表&#xff0c;并初始化了其中的两条数据。 DROP TABLE IF EXISTS tbl_order; CREATE TABLE tbl_order (id bigint(0) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 自增主键,order_no varchar(50)…