性能杀手—伪共享

来源:互联网 发布:ui平面设计软件 编辑:程序博客网 时间:2024/04/29 19:14
性能杀手—伪共享

在之前的一篇文章<java缓存行与volatile>中有讲到缓存行(可以到我的公众号中查找,公众号二维码在下方),缓存行是CPU缓存的最小单位,缓存行的大小不固定,根据具体CPU架构而定,一般是64BYTE。而就是因为缓存行的存在,导致了一个潜在的性能问题—伪共享。


了解缓存行带来的性能问题前先来了解以下概念:

一、CPU的缓存结构,现代CPU除去寄存器外就是三级缓存(L1、L2、L3),其中L1、L2是CPU各个核心独有的,而L3是所有核心共有的,CPU读取一个数据首先会从寄存器中读取,然后是从三级缓存中查找,如果在缓存查找不到则会去内存查找,图示如下:

cache.png

同时在三级缓存中L1又是最快的,所以对于比较频繁使用的数据尽量保证能在L1中命中是最好的。


二、CPU读取一个数据时如果该数据不能完全填充一个缓存行,那么CPU将会多读取一部分数据进来填充缓存行。


有了以上基础,我们重新来看我们的问题,在java中,如果想要保证一个变量的修改在线程间是可见的,那么需要加上volatile关键字修饰,volatile会保证CPU不会读取到过期的数据,那么具体是如何做的呢?首先CPU的某个核心A会将volatile关键字修饰的变量读取(copy一份副本)到L1缓存或L2缓存中以供后续使用,同时另外一个核心B也会做同样的操作,在过了一段时间后,核心B将该变量的值修改了,由于此变量是volatile修饰的,该值会被同步到L3缓存,同时核心A会得到通知(如果没有volatile关键字修饰,那么核心A可能永远不知道该变量已经被修改失效了),知道该缓存已经失效,核心A会重新从L3中获取该变量的值。


volatile修饰的变量修改大概逻辑就是这样,乍一看好像没有什么问题,但是结合前边的基础——CPU不仅会将当前需要使用的数据读取进来,由于缓存最小单位是缓存行,也就是如果该缓存行如果没有被填充满CPU会顺带往后继续读取一部分数据,问题就出来了。具体问题如下:


假设现在有两个变量,两个变量的大小都是32byte(只要两个变量大小都不等于64byte即可),同时都用volatile修饰,又有两个核心分别使用它们,也就是核心A使用变量a,核心B使用变量b,而恰巧a和b又在同一缓存行上,那么现在问题来了,如果核心A修改了变量a的值,由于volatile的原因和a与b在同一缓存行的前提下,核心B的缓存b也会失效,为什么?因为核心A修改了a的值使该缓存行失效了,而b也在该缓存行上,虽然b的值没有被A修改,但是b在核心B的缓存依然失效了,也就是说B此时需要重新从比较慢的L3缓存读取最新值,从逻辑上来讲a和b是互不相干的,但是在底层的CPU缓存操作上他们却又是互相影响的,而L1、L2缓存的失效则会导致CPU读取速度变慢,最终导致程序变慢,该场景图示如下:

缓存行.png

该问题称为伪共享,因为你访问a(缓存a)的同时也会把你不需要的b缓存下来。

那么如何应对这种情况呢?有一个很简单同时被很多高性能框架采用的办法,那就是缓存行填充,即通过一些无用的字段将你的volatile修饰的对象(变量)填充成64byte,不过该解决方案在某些情况下依然存在问题,例如缓存行大小不是64byte的情况下该解决方案就会失效。

该问题在JDK8中可以使用注解sun.misc.Contended解决,JVM会自动将使用该注解的变量分配到不同的缓存行。


时间紧,写的比较凌乱,就大概看下有个了解即可,深入了解的话可以自行再去看相关方面的书籍文献,也可以与我一起讨论研究~


没有关注的可以扫下方二维码关注我(公众号),如果在使用过程中有任何问题还可以加我QQ1213812243询问~



         

长按二维码关注我吧

不要错过




原创粉丝点击