阿里云一面:并发场景下的底层细节-伪共享问题

最近看书看到的伪共享问题,直接触碰到知识盲区了,之前确实没听说过这个东西,打开百度就像吃饭一样自然。

虽然面经上出现的次数不多,不过我觉得还是很重要的一个问题,而且不难,花个五分钟就能弄清楚~

老规矩,背诵版在文末。公众号【 飞天小牛肉】定期更新大厂面试题,提供背诵版和详解版

三级缓存架构

众所周知,为了缓解内存和 CPU 之间速度不匹配的矛盾,引入了高速缓存这个东西,它的容量比内存小很多,但是交换速度却比内存要快得多。

阿里云一面:并发场景下的底层细节-伪共享问题

之前我们画过这样的分级存储体系结构:

阿里云一面:并发场景下的底层细节-伪共享问题

事实上,高速缓存仍然存在细分,也称为 三级缓存结构:一级(L1)缓存、二级(L2)缓存、三级(L3)缓存

阿里云一面:并发场景下的底层细节-伪共享问题

越靠近 CPU 的缓存,速度越快,容量也越小。所以 L1 缓存容量最小但是速度最快;L3 缓存容量最大同时速度也最慢

当 CPU 执行运算的时候,它会先去 L1 缓存查找所需的数据、如果没有找到的话就再去 L2 缓存、然后是 L3 缓存,如果最后这三级缓存中都没有命中,那么 CPU 就要去访问内存了。

显然, CPU 走得越远,运算耗费的时间就越长。所以尽量确保数据存在 L1 缓存中能够提升大计算量情况下的运行速度

需要注意的是,CPU 和三级缓存以及内存的对应使用关系:

  • L1 和 L2 都是只能被一个单独的 CPU 核心使用
  • L3 可以被单个插槽上的所有 CPU 核心共享
  • 内存由全部插槽上的所有 CPU 核心共享

如下图所示:

阿里云一面:并发场景下的底层细节-伪共享问题

另外,这三级缓存空间中的数据是如何组织起来的呢?换句话说,数据在这三级缓存中的存储形式是什么样的呢?

Cache Line(缓存行)!

缓存中的基本存储单元就是 Cache Line

每个 Cache Line 通常是 64 字节,也就是说,一个 Javalong 类型变量是 8 字节,一个 Cache Line 中可以存 8 个 long 类型的变量。

所以小伙伴们看出来了吗~ 缓存中的数据并不是按照一个一个单独的变量这样存储组织起来的,而是多个变量会放到一行中

阿里云一面:并发场景下的底层细节-伪共享问题

伪共享问题 False Sharing

说了这么多似乎还并未触及伪共享问题,别急,我们离真相已经很近~

在程序运行的过程中, 由于缓存的基本单元 Cache Line 是 64 字节,所以缓存每次更新都会从内存中加载连续的 64 个字节

如果访问的是一个 long 类型数组的话,当数组中的一个值比如 v1 被加载到缓存中时,接下来地址相邻的 7 个元素也会被加载到缓存中。(这也能解释为啥我们数组总是能够这么快,像链表这种离散存储的数据结构,就无法享受到这种红利)。

But,这波红利很可能带来无妄之灾。

举个例子, 如果我们定义了两个 long 类型的变量 a 和 b,他们在内存中的地址是紧挨着的,会出现什么问题

如果我们想要访问 a 的话,那么 b 也会被存储到缓存中来。

懵了懵了,这有什么问题吗,看起来似乎没有什么毛病,多么 nice 的特性啊

来来来,直接上个例子

回想下上文提到的 CPU 和三级缓存以及内存的对应使用关系,设想这种情况,如果一个 CPU 核心的线程 T1 在对 a 进行修改,另一个 CPU 核心的线程 T2 却在对 b 进行读取。

当 T1 修改 a 的时候,除了把 a 加载到 Cache Line,还会享受一波红利,把 b 同时也加载到 T1 所处 CPU 核心的 Cache Line 中来,对吧。

根据 MESI 缓存一致性协议,修改完 a 后这个 Cache Line 的状态就是 M(Modify,已修改),而其它所有包含 a 的 Cache Line 中的 a 就都不是最新值了,所以都将变为 I 状态(Invalid,无效状态)

这样,当 T2 来读取 b 时,诶,发现他所处的 CPU 核心对应的这个 Cache Line 已经失效了,mmp,它就需要花费比较长的时间从内存中重新加载了。

阿里云一面:并发场景下的底层细节-伪共享问题

问题已经显而易见了, b 和 a 没有任何关系,每次却要因为 a 的更新导致他需要从内存中重新读取,拖慢了速度。这就是伪共享

表面上 a 和 b 都是被独立线程操作的,而且两操作之间也没有任何关系。只不过它们共享了一个缓存行,但所有竞争冲突都是来源于共享。

用更书面的解释来定义伪共享: 当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,导致无法充分利用缓存行特性,这就是伪共享

伪共享解决方案

我们先来举个例子看看一段伪共享代码的耗时,如下所示,我们定义一个 Test 类,它包含两个 long 的变量,分别用两个线程对这两个变量进行自增 1 亿次,这段代码耗时 5625ms

阿里云一面:并发场景下的底层细节-伪共享问题

对于伪共享,一般有两种方法,其实思想都是一样的:

1) padding:就是增大数组元素之间的间隔,使得不同线程存取的元素位于不同的缓存行上,以空间换时间

上面提到过,一个 64 字节的 Cache Line 中可以存 8 个 long 类型的变量,我们在 a 和 b 这两个 long 类型的变量之间再加 7 个 long 类型,使得 a 和 b 处在不同的 Cache Line 上面:

阿里云一面:并发场景下的底层细节-伪共享问题
class Pointer {
    volatile long a;
    long v1, v2, v3, v4, v5, v6, v7;
    volatile long b;
}

再次运行程序,会发现输出时间神奇的缩短为了 955ms

2) JDK1.8 提供了 @Contended 注解:就是把我们手动 padding 的操作封装到这个注解里面了,这个注解可以放在类上也可以放在字段上,这里就不多做说明了

class Test {
    @Contended
    volatile long a; // 填充 a
    volatile long b;
}

需要注意的是,默认使用这个注解是无效的,需要在 JVM 启动参数加上 XX:-RestrictContended 才会生效

最后放上这道题的背诵版:

🥸 面试官:讲一下伪共享问题
😎 小牛肉:伪共享问题其实是由于高速缓存的特性引起的,三级高速缓存中的数据并不是一个变量一个变量单独存放的,它的基本存储单元是 Cache Line,一般一个 Cache Line 的大小是 64 字节,也就是说,一个 Cache Line 中可以存下 8 个 8 字节的 long 类型变量
那由于高速缓存的基本单元是 64 字节的 Cache Line,所以呢,缓存从内存中一次读取的数据就是 64 字节。换句话说,如果 cpu 要读取一个 long 类型的数组,读取其中一个元素的同时也会把接下来的其他相邻地址的七个元素也加载到 Cache Line 中来。
这就会导致一个问题,举个例子,我们定义了两个 long 类型的变量 a 和 b,他们没有关系,但是他们在内存中的地址是紧挨着的,如果一个 CPU 核心的线程 T1 在对 a 进行修改,另一个 CPU 核心的线程 T2 却在对 b 进行读取。
那么当 T1 修改 a 的时候,除了把 a 加载到 Cache Line,还会把 b 同时也加载到 T1 所处 CPU 核心的 Cache Line 中来,对吧。
根据 MESI 缓存一致性协议,修改完 a 后这个 Cache Line 的状态就是 M(Modify,已修改),而其它所有包含 a 的 Cache Line 中的 a 就都不是最新值了,所以都将变为 I 状态(Invalid,无效状态)
这样,当 T2 来读取 b 时,他就会发现,他所处的 CPU 核心对应的这个 Cache Line 已经失效了,这样他就需要花费比较长的时间从内存中重新加载了。
也就是说,b 和 a 没有任何关系,每次却要因为 a 的更新导致他需要从内存中重新读取,拖慢了速度。这就是伪共享

对于伪共享,一般有两种方法,其实思想都是一样的:
1)padding:就是增大数组元素之间的间隔,使得不同线程存取的元素位于不同的缓存行上,以空间换时间。比如在 a 和 b 之间再定义 7 个 long 类型的变量,使得 a 和 b 不在一个 Cache Line 上,这样当修改 a 的时候 b 所处的 Cache Line 就不会受到影响了
2)JDK 1.8 提供了 @Contended 注解,就是把我们手动 padding 的操作封装到这个注解里面了,把这个注解加在字段 a 或者 b 的上面就可以了

大家好,我是小牛肉~ 如果觉得这篇文章有用的话请一键三连呀,我的公众号【 飞天小牛肉】定期更新大厂面试题,提供背诵版和详解版

Original: https://www.cnblogs.com/cswiki/p/16373592.html
Author: 飞天小牛肉
Title: 阿里云一面:并发场景下的底层细节-伪共享问题

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/588365/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 哈工大计算机系统大作业(2022)

    (论文封面略去) 本人博客链接(防扒): 本人CSDN博客: 何以牵尘的博客_CSDN博客-哈工大课内学习,哈工大精品课程笔记领域博主何以牵尘擅长哈工大课内学习,哈工大精品课程笔记…

    Java 2023年6月9日
    082
  • 生成一个session id

    var genSessionId = function(length){ var str = genSessionId.characters; if ( !"0&quot…

    Java 2023年5月30日
    093
  • 面试题详解:如何用Redis实现分布式锁?

    说一道常见面试题: 使用Redis分布式锁的详细方案是什么? 一个很简单的答案就是去使用 Redission 客户端。Redission 中的锁方案就是 Redis 分布式锁的比较…

    Java 2023年6月7日
    088
  • Vue3 封装 Element Plus Menu 无限级菜单组件

    本文分别使用 SFC(模板方式)和 tsx 方式对 Element Plus el-menu 组件进行二次封装,实现配置化的菜单,有了配置化的菜单,后续便可以根据路由动态渲染菜单。…

    Java 2023年6月16日
    064
  • 日志的搭建

    @ 前言 一、依赖 二、日志文件 三、代码编写 四、日志输出 提示:本文仅供学习交流,请勿用于非法活动! 前言 本文内容: 日志搭建 一、依赖 ch.qos.logback log…

    Java 2023年6月13日
    075
  • 专门为小白准备的入门级mybatis-plus-generator代码自动生成器,提高开发效率。值得收藏

    引入依赖 com.baomidou mybatis-plus-generator 3.5.2 org.apache.velocity velocity-engine-core 2….

    Java 2023年6月8日
    078
  • 1438. Longest Continuous Subarray With Absolute Diff Less Than or Equal to Limit

    题目: Given an array of integers nums and an integer limit, return the size of the longest n…

    Java 2023年5月29日
    075
  • EVE-NG 安装和使用过程中出现的问题

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    Java 2023年6月9日
    049
  • Day13

    package com.oop.demo05;//在java中所有的类都直接或者间接默认继承object//人 父类public class Person { //public 公…

    Java 2023年6月5日
    0105
  • Java动态代理模式

    1、怎样实现静态代理模式? 可以想到的方式起码有两种继承和聚合。 创建一个接口 package com.jyd.proxy; /** * 定义一个能够工作的接口。定义一系列操作方法…

    Java 2023年5月29日
    0120
  • 数据库篇:mysql锁详解

    前言 sql事务的执行,如果需要锁定数据进行更新操作,则必定离不开锁 共享锁和排他锁 表锁 行锁 Record Lock 间隙锁 Gap Lock 行锁+间隙锁 Next-Key …

    Java 2023年6月5日
    085
  • Java中的final关键字

    字面意思:被final修饰的对象是无法改变的。 一、final数据: (2)同时被static和final修饰的数据占据一段不能被改变的存储空间,同时,由于static的作用,该数…

    Java 2023年5月29日
    097
  • Spring Boot:整合Shiro权限框架

    综合概述 Shiro是Apache旗下的一个开源项目,它是一个非常易用的安全框架,提供了包括认证、授权、加密、会话管理等功能,与Spring Security一样属基于权限的安全框…

    Java 2023年5月30日
    061
  • Spring Cloud Gateway 不小心换了个 Web 容器就不能用了

    最近组员修改微服务的一些公共依赖,在某个依赖中需要针对我们微服务使用的 Undertow 容器做一些订制,所以加入了 web 容器 Undertow 的依赖。但是,一般这种底层框架…

    Java 2023年6月7日
    0149
  • 躬行算法之最小的最大值

    最近看到这样一道面试题, 求最小的最大值,觉得挺有意思,在这里分享下。 给定一个数组 a,包含 n 个整数。再给定一个整数 k,可以给数据中任意整数加 1,总共可以加 k 次。加完…

    Java 2023年6月8日
    067
  • Spring Boot 集成 Thymeleaf

    使用 idea 新建一个 demo 项目 pom.xml 在 templates 文件夹中 新增一个 index.html controller 层 启动后,进行访问得到的效果 访…

    Java 2023年5月30日
    086
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球