【Java 并发编程】(三) 从CPU缓存开始聊 volatile 底层原理

news2024/9/23 12:48:12

并发编程

三大问题

  • 在并发编程中,原子性、有序性和可见性是三个重要的问题,解决这三个问题是保证多线程程序正确性的基础。
  • 原子性: 指的是一个操作不可分割, 要么全部执行完成, 要么不执行, 不存在执行一部分的情况.
  • 有序性: 有序性是指程序的执行顺序与程序中代码的顺序一致。在多线程环境下,由于线程的交替执行和指令重排等因素,可能会导致代码的执行顺序与预期不一致
  • 可见性: 可见性是指当一个线程修改了共享变量的值时,其他线程能够立即看到这个修改。在多核处理器和多级缓存系统中,线程对共享变量的修改可能被缓存到CPU的本地缓存中,而其他CPU上的线程无法立即看到这个修改,从而导致数据不一致的问题。
  • 原子性问题, 可以用 synchronized 关键字解决, JDK 也提供了 ReentrantLock 等机制, 也能解决;
  • 有序性和可见性, 可以由 volatile 解决;

原子性问题, 就不多说了, 下面重点介绍一下如何解决有序性和可见性问题;

HappensBefore原则

  • 它是一种顺序保证,确保在并发环境下的有序性和可见性;
  • 例如, 该规则规定同一个线程内的每个操作, 都 happens-before 于该线程中的任意后续操作;
  • 例如, 一个监视器锁上一次的解锁操作, happens-before于下一次的加锁操作;
  • 例如, 如果线程A调用线程B的start()方法来启动线程B,则start()操作Happens-Before于线程B中的任意操作。
  • 如果 A happens before B, 那么 A 应该在 B 之前执行, 且 A 的结果应该对 B 可见;

原则固然好, 问题是怎么实现呢? 主要就是 synchronized + volatile, synchronized已经介绍过, 这里介绍 volatile, 让我们先从缓存开始说起;

缓存行

  • 现代计算机为了缓和 CPU 速度和内存速度之间的差异, 会在内存与CPU之间设置多级缓存, 缓存的速度比内存快;

  • 当 CPU 读缓存未命中时, 会从内存读取数据并放入到缓存中, 以后就可以直接从缓存中读取, 提高了速度;

  • 从内存往 CPU Cache 读的时候, 根据程序局部性原理, 会按块(在缓存里也叫缓存行)读取, 大小为64B;

  • 如果你有一个特别热点的变量, 那应该让他尽量独占一个缓存行, 怎么做? 在前后填充无意义数据, 前后都填充 7 * 8B, 这样就保证热点变量一定独占一个缓存行;

  • 现在 CPU 一般都是多核的, 每个核相当于一个独立的 CPU;

  • 现在的 CPU 缓存一般是三级缓存, 一二级在 CPU 核心内部, 三级共享;

在这里插入图片描述

  • 补充: 超线程

    一个CPU内有一套ALU计算单元, 两套程序计数器PC和寄存器, 这样就可以同时保存两个线程的上下文, 切换时只需要让ALU切换一下数据来源即可;

    这样提高了线程切换效率, 8核16线程就是这么来的;

英特尔X86 - MESI

  • 每个CPU内核有自己的 cache, 为了解决不同 CPU内核的 Cache 之间以及 Cache 与主存之间的一致性问题, 引入了串行总线 + MESI协议的解决方式;

  • 将 Cache 中缓存的数据分为四种状态;

    1. **Exclusive(E):**当某个缓存数据仅存在于一个CPU核内, 并且与内存中的值一致时, 该缓存行的状态为 Exclusive。
    2. Modified(M): 在E的基础上, 如果内核修改了缓存, 使得与内存不一致, 该缓存行的状态为 Modified;
    3. Shared(S): 当一个缓存行被多个CPU内核缓存,并且缓存中的数据与内存中的数据一致时,该缓存行的状态为 Shared;
    4. Invalid(I): 在S的基础上, 某个内核修改自己的缓存时, 其它内核的缓存将被失效, 状态变为 Invalid
  • 举例

    1. CPU0 读变量a, a 从主存缓存到CPU0, 状态为 E;

    2. CPU0 写变量a, 缓存状态改为 M;

    3. CPU1 读变量a, 发现 CPU0 有变量a的缓存, 那么拿到自己的缓存里来, 并将缓存的最新值写入内存, 缓存的状态变为S, CPU0和1现在都有a的缓存, 且状态都是 S;

    4. CPU0 再次修改a, 这会将 CPU1 的缓存失效, 状态改为 I; 并将最新值写入内存, CPU0自己的缓存状态改为 E;

  • 当CPU内核去查询其它CPU内核是否有相同缓存, 以及通知其它CPU缓存失效等操作时, 为了避免这些操作发生混乱, 总线是串行的;

  • 补充: 如果数据非常大, 一个缓存行放不下, 怎么保证一致性? 直接到内存中访问, 并且访问时锁总线;

store buffer & invalidate queue

  • 因为总线是串行的, 所以效率较低, 为此引入了store bufferinvalidate queue; 以下简称 SB 和 IQ;

  • 每个 CPU 都有自己 SB 和 IQ ;

  • 前面讲过, 当一个 CPU 要读某个数据时, 会向其它 CPU 查询是否有该数据的缓存, 如果有, 拿过来, 如果没有, 去内存拿; 这个过程是锁总线的, 是串行的, 过程中所有 CPU 都不能使用总线;

  • 当一个 CPU 要失效其它 CPU 中的数据时也是一样, 其它 CPU 要等待通知, 然后失效对应的缓存, 这个过程中不能去使用总线;

  • 现在引入 SB 后, 当 CPU 要读取数据时, 由 SB 与其它 CPU 交互, 得到的结果暂存到 SB 中, CPU 此时可以去执行其它指令;

  • IQ 也是一样, 当有失效通知到来时, 先缓存到 IQ 中, CPU再异步地进行处理;

指令重排

指令重排通常出现在以下两个阶段:

编译器优化阶段:编译器在生成字节码或机器码时,为了提高执行效率,可能会对源代码中的指令进行重新排序。例如,编译器可能会将没有依赖关系的指令提前执行,以充分利用 CPU 的流水线能力。

处理器优化阶段:处理器为了最大化硬件资源的利用率,可能会在执行指令时重新调整指令的顺序。例如,在处理器的流水线中,如果某个指令的执行依赖于之前指令的结果,而该结果尚未准备好,处理器可能会先执行其他指令。

比如 SB 和 IQ 的引入, 就会导致修改不能立即可见以及指令重排的问题;

// 假设一开始 flag 值为true, 在线程 1 和线程 2都有缓存;
// 线程1先执行, 这将导致线程2的缓存失效, 但是因为invalidQueue, 线程2并不会立即收到这一信息;
{
    flag = false;
}

// 线程二可能还没来得及处理IQ中的失效通知, 导致还是能通过 if 判断;
if(flag){
    // 导致还能进来;
    i++;
}

// 明明我先把一个值改为 false 了, 其它线程却还是判断为 true, 这就发生了不可见;
// 本来应该 flag = false 然后 i++ 不执行, 现在却变成了相当于线程二先通过判断并执行 i++, 线程一再 flag = false
// 这就发生了指令重排;

指令重排问题举例: new对象

一次完整的 new 对象并执行构造方法的过程, 其字节码如下

new #2 <T>
dup
invokespecial #3 <T.<init>>
astore_1
return
  • new 分配空间, 并将该引用压到操作数栈; 分配以后所有成员都是默认值;

    分配空间的时候有两种方式: 指针碰撞和空闲链表;

    首先, 不考虑逃逸分析的话, 新对象的创建都在堆上;

    Eden 区放得下, 就在 Eden 区分配; 如果是超大对象, 还有可能直接在老年代分配;

    指针碰撞: 用一个指针指向当前空闲区域的起始位置;

    适用于不会产生碎片的垃圾回收算法, 比如 Parallel Scavenge, 基于复制算法; 所以, 新生代上 new 对象, 一般适用指针碰撞;

    空闲链表: 维护空闲链表, 每个元素对应一个空闲块; 适用于会产生碎片的垃圾回收算法; 比如CMS;

    如何解决多线程同时分配内存的安全问题?

    可以用 CAS;

    可以用 TLAB; 每个线程初始化的时候, 分配一个 在分配内存权限上私有的 一块Buffer; 满了再申请; 分配是私有的, 访问不是;

  • dup 将栈顶的值复制一份再次入栈;

  • invokespecial 弹出栈顶, 作为 this 传给构造方法;

  • astore, 将弹出栈顶, 保存到当前方法的局部变量表中;

  • return, 返回;

  • 由于指令重排, 有可能还没调用构造方法, 就放到局部变量表里了, 这时候去使用它, 用的是一个没有经过构造方法初始化的对象, 很危险;

指令重排问题举例: 单例模式

如何做一个线程安全的懒加载单例类? 大多数人的回答是DCL, 即 double check lock

private static singleton;
public static Singleton get(){
    // 外层的if 保证效率, 已经创建了单例对象的时候不会进入synchronized;
    if(singleton == null){
        synchronized(Singleton.class){
            // 内层保证多线程安全
        	if(singleton == null)
                singleton = new Singleton();
        	else{
        		return singleton;
    		}
    	}
    }
    else{
        return singleton;
    }
}
  • 正确的回答要在 DCL 的基础上, 给 singleton 引用加 volatile, 如果不加volatile, DCL也没用

    private volatile static singleton;
    
  • 因为new对象是个过程, 假设没有加volatile, 因为指令重排, 使得astore指令在 invokespecial 指令前执行; 那么线程一 new 对象 new 到一半, 所有成员还是默认值的情况下, 就把引用保存了, 这时如果线程2到来, 进行外层判断, singleton != null, 会直接把这个没有执行invokespecial的对象返回;

  • 如果是一个初值为1000的账户, 那现在初始金额只有0;

volatile 如何解决可见性与有序性问题?

  1. 在源码中加volatile关键字, 编译为 class 文件后, 对应 ACC_VOLATILE 指令;

  2. CPU 提供了内存屏障指令, 上层应用可以在合适的地方添加内存屏障指令来避免指令重排;

    JVM 会自动对 volatile 变量的读写操作添加对应的内存屏障;

    比如对 volatile 修饰的变量 x 进行写操作:

    JVM 自动在写操作之前加 StoreStore 屏障, 表示前面的对普通变量的写操作完成, 当前的写操作才能执行;

    后面加 StoreLoad, 表示当前的写操作执行完了, 后面对普通变量的读操作才能执行;

  3. 读写屏障在底层使用 lock 汇编指令, 通过对总线或者缓存行加锁的方式, 禁用 SB 和 IQ, 将对缓存的修改强制立即写入主存, 进而解决了可见性和有序性问题;

  4. 需要注意, volatile 并不保证原子性; 不过, 在一些场景下, 比如 CAS 操作一个变量, 通过 CAS 和 volatile 是可以同时解决三大问题的, 性能比synchronized 要好;

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

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

相关文章

Arco Chatter - 改变 Chatter Panel 交互体验的插件

关于 Chatter Panel Arco Chatter 插件是一款专门为 odoo 系统 Chatter Panel 设计的实用工具&#xff0c;它具有改变 Chatter Panel 展开/收起状态和调整 Chatter Panel 宽度的功能。 Arco Chatter 插件主要用于优化用户在 odoo 系统中的操作体验。通过插件功能&#xff0c;用…

【iOS】—— JSONModel

JSONModel源码 1. JSONModel介绍2. JSONModel的其他用法2.1 转换属性名称2.2 自定义错误 3. 源码分析3.1 - (id)initWithDictionary:(NSDictionary*)dict error:(NSError**)err3.2 JSONModel持有的数据3.3 load3.4 JSONModel的init方法3.5 __inspectProperties方法3.6 JSONMode…

动态创建 Delphi 按钮的完整指南:基于配置文件的 `TGridPanel` 实现

在 Delphi 开发中&#xff0c;我们经常需要根据不同的配置动态生成 UI 元素。本文将带你通过一个完整的示例&#xff0c;演示如何根据配置文件动态创建按钮&#xff0c;并将它们排列在一个 TGridPanel 中。每个按钮的标题、链接、颜色和大小都将从配置文件中读取。 “C:\myApp\…

基于YOLOv8-pose的手部关键点检测(1)- 手部关键点数据集获取(数据集下载、数据清洗、处理与增强)

前言 手部姿态估计、手势识别和手部动作识别等任务时&#xff0c;可以转化为对手部关键点的分布状态和运动状态的估计问题。本文主要给出手部关键点数据集获取的方式。 总共获取三个数据集&#xff1a; handpose_v2&#xff1a;训练集35W张&#xff0c;验证集2.85W张&#xff1…

vim - vim模式及部分操作

文章目录 一、vim 基本介绍二、vim 的简单使用三、几种常用模式切换四、命令模式和底行模式的操作汇总 一、vim 基本介绍 vim 是一款多模式的编辑器。vim 中有很多子命令来进行代码的编写操作。 同时&#xff0c;vim 提供了不同的模式供我们选择。 在vim下的底行模式下通过:he…

如何查询婚姻状况信息?

1.使用在线查询工具&#xff1a;‌ 现在&#xff0c;‌也有一些在线查询工具&#xff0c;‌如“天远查”“全能查”等微信小程序&#xff0c;‌提供了婚姻状态查询服务。‌这些工具通常需要你提供一些基本信息&#xff0c;‌并可能收取一定的费用。‌在使用这些工具时&#xff…

WebRTC为何成为视频开发领域的首选技术? EasyCVR视频转码助力无缝视频通信

随着互联网的飞速发展&#xff0c;视频通信已成为日常生活和工作中不可或缺的一部分。从在线教育、视频会议到远程医疗、在线直播&#xff0c;视频开发的需求日益增长。在这些应用场景中&#xff0c;选择何种技术来构建视频系统至关重要。 目前&#xff0c;在很多视频业务的开…

文本纠错实现定位与标记

一、基于讯飞文本纠错实现前端标记定位&#xff0c;点击可以联动&#xff01;

VM下kali设置桥接网络

一、查看主机ip 1.winr输入cmd 2.进入终端输入ipconfig 3.查看ip 二、虚拟机网络设置 1.进入vm的虚拟网络编辑器 2.桥接网卡自己选&#xff0c;1是有线网卡2是无线网卡&#xff0c;选择记得点应用 3.虚拟机的网络适配器也要选择桥接模式 三、kali网络配置 1.打开kali终端编辑文…

基于Spring Boot的库存管理系统

TOC springboot265基于Spring Boot的库存管理系统 绪论 1.1 选题动因 在现在社会&#xff0c;对于信息处理方面&#xff0c;是有很高的要求的&#xff0c;因为信息的产生是无时无刻的&#xff0c;并且信息产生的数量是呈几何形式的增加&#xff0c;而增加的信息如何存储以及…

linux下QOS:理论篇

关于qos &#xff0c;也是linux下面必备功能之一&#xff0c;一般只需要结合iptables/etables/iproute2 和tc配合即可实现大部分功能. 网上讲这么方面的资料很多&#xff0c;大部分都讲tc命令的应用.这里就先从理论入手. QoS&#xff08;Quality of Service&#xff09;服务质…

WSL2 使用usbipd工具 连接USB设备

Connect USB devices | Microsoft Learn 使用开源工具usbipd&#xff0c;可以让usb设备连接WSL https://github.com/dorssel/usbipd-win usbipd list 可以查看连接到win上的设备。 把USB设备从win转移到WSL需要执行下面两个指令。 usbipd bind --busid <BUSID> usb…

Shiro-721 分析

前言 shiro-550漏洞的产生源自硬编码问题&#xff0c;在 1.2.4之前&#xff0c;密钥在代码中是固定的 而在1.2.5 < Apache Shiro < 1.4.1&#xff0c;我们再看AbstractRememberMeManager类&#xff0c;已经修改为生成随机的密钥 在本篇文章中&#xff0c;分析着重于代…

电子电气架构---EEA的发展趋势

我是穿拖鞋的汉子&#xff0c;魔都中坚持长期主义的汽车电子工程师。 老规矩&#xff0c;分享一段喜欢的文字&#xff0c;避免自己成为高知识低文化的工程师&#xff1a; 屏蔽力是信息过载时代一个人的特殊竞争力&#xff0c;任何消耗你的人和事&#xff0c;多看一眼都是你的不…

RPA自动化流程机器人在财税管理中的作用

随着科技的飞速发展&#xff0c;人工智能和自动化技术正在改变市场竞争的格局&#xff0c;企业对于提高工作效率和准确性的要求日益提高。财税管理作为企业运营管理的核心环节之一&#xff0c;其数字化程度决定了企业发展的速度、广度和深度。因此&#xff0c;财税数字化成为各…

transformer-explainer

安装和启动 找到这个项目&#xff0c;然后装好了。 这个项目的目的如名字。 https://github.com/poloclub/transformer-explainerTransformer Explained: Learn How LLM Transformer Models Work with Interactive Visualization - poloclub/transformer-explainerhttps:/…

【算法】蚁群算法

一、引言 蚁群算法&#xff08;Ant Colony Optimization, ACO&#xff09;是一种模拟蚂蚁觅食行为的启发式搜索算法。它由Marco Dorigo于1992年提出&#xff0c;适用于解决组合优化问题&#xff0c;如旅行商问题&#xff08;TSP&#xff09;、车辆路径问题&#xff08;VRP&…

STM32初识

这边软件使用的是Keil5&#xff0c;主要介绍一下使用的一些注意事项。 创建工程部分&#xff1a; 创建工程方式有两种&#xff1a;使用Keil创建工程模板、 使用STM32CubeMX 新建一个工程 1.新建一个文件&#xff0c;添加文件&#xff1a; DOC工程说明 doc说明文档 Librarie…

联想集团2025届校招网申认知能力SHL测评深度解析

引言 随着联想集团校招季的到来&#xff0c;众多求职者正摩拳擦掌&#xff0c;准备在这场竞争激烈的选拔中脱颖而出。认知能力测评作为选拔过程中的重要环节&#xff0c;其重要性不言而喻。本文将对联想集团校招中使用的认知能力测评进行深度解析&#xff0c;为求职者提供全面而…

K8S - ConfigMap的简介和使用

什么是configMap Kubernetes中的ConfigMap 是用于存储非敏感数据的API对象&#xff0c;用于将配置数据与应用程序的镜像分离。ConfigMap可以包含键值对、文件或者环境变量等配置信息&#xff0c;应用程序可以通过挂载ConfigMap来访问其中的数据&#xff0c;从而实现应用配置的…