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

最近看书看到的伪分享问题,直接触及了知识盲区。我以前从未听说过这件事。打开百度就像吃饭一样自然。

虽然在上的次数不多,但我觉得还是很重要的一个问题,而且也不难,五分钟就能搞定~

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

L3缓存架构

众所周知,为了缓解内存和CPU速度不匹配的矛盾,引入了缓存。它的容量比内存小得多,但交换速度却比内存快得多。

之前我们画了一个这样的分层存储架构:

其实缓存还是有细分的,也就是所谓的三级缓存结构:一级(L1)缓存,二级(L2)缓存,三级(L 3) 缓存)

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

CPU执行操作时,会先去L1缓存寻找需要的数据,如果没有找到,就会去L2缓存,再去L3缓存,如果最后三级缓存都没有命中,然后CPU会去内存被访问。

显然,CPU 走得越远,计算所需的时间就越长。所以尽量保证数据存储在L1缓存中,以提高计算量较大的情况下的运行速度。

需要注意的是CPU与L3缓存和内存的对应使用关系:

如下所示:

另外,三级缓存空间中的数据是如何组织的?也就是说,这个三级缓存中数据的存储形式是什么?

缓存线!

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

每个 Cache Line 通常是 64 字节,即一个 Java long 类型变量是 8 个字节,一个 Cache Line 可以存储 8 个 long 类型变量。

所以你们能看出来吗~缓存中的数据不是作为单个变量存储和组织的,而是多个变量会放在一行中。

虚假分享问题 False

说了这么多,看来伪分享的问题还没有触及到。别着急,我们离真相很近了~

在程序运行过程中,由于缓存的基本单位是64字节,每次更新缓存时,缓存都会从内存中连续加载64字节。

如果访问一个long类型的数组,当数组中的一个值比如v1被加载到缓存中时,接下来的7个地址相邻的元素也会被加载到缓存中。(这也解释了为什么我们的数组总是那么快,而像链表这样的离散存储数据结构却不能享受这个好处)。

但是,这波红利很可能带来灾难。

比如我们定义了两个long类型的变量a和b,它们在内存中的地址是相邻的,会发生什么?

如果我们要访问a,那么b也将存储在缓存中。

我很困惑,这有什么问题吗?它似乎没有任何问题,这是一个多么好的功能。

来吧,我们举个例子

回想上面提到的CPU与L3缓存和内存的对应使用关系,想象一下这种情况,如果一个CPU核心线程T1在修改a,而另一个CPU核心线程T2在读取b。挑选。

当T1修改a时,除了将a加载到Cache Line之外,还会享受一波加成,还将b加载到T1所在CPU核心的Cache Line,对吧。

根据MESI缓存一致性协议,修改a后,Cache Line的状态为M(修改),而其他所有包含a的Cache Line中的a都不是最新值,所以会变成I状态( ,无效状态)

这样,T2在读取b的时候,嘿嘿,他发现自己所在的CPU核心对应的Cache Line已经过期了,mmp,从内存中重新加载需要很长时间。

问题已经很明显了,b和a没有关系,但是每次因为a的更新需要从内存中重新读取,速度就变慢了。这是虚假分享

表面上 a 和 b 是由独立的线程操作的,这两个操作之间没有任何关系。只是他们共享一个缓存行,但所有的争用冲突都源于共享。

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

伪共享解决方案

我们举个例子来看看伪共享代码的耗时。如下图,我们定义一个Test类,其中包含两个long变量,并使用两个线程自动将这两个变量递增1亿次。,这段代码需要时间

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

1):就是增加数组元素之间的间隔,让不同线程访问的元素位于不同的缓存行,空间换时间

如上所述,一个 64 字节的 Cache Line 可以存储 8 个 long 类型的变量。我们在 a 和 b 两个长型变量之间添加了 7 个长型变量,使 a 和 b 处于不同的位置。上面的缓存线:

class Pointer {
    volatile long a;
    long v1, v2, v3, v4, v5, v6, v7;
    volatile long b;
}

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

2)JDK1.8 提供了@注解:就是将我们手动操作封装到这个注解中。此注释可以放在类上或字段上。我不会在这里解释它。跨度

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

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

最后,把这个问题的记忆版本:

分类:

操作系统的东西

技术要点:

相关文章:

© 版权声明
THE END
喜欢就支持一下吧
点赞82 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片