探讨缓存行与伪共享
最近项目中有个需求,需要用到有界队列对访问请求量进行流量削峰请求,同时作为一个缓冲层对请求处理进行后续处理,Java 内置有界队列 ArrayBlockingQueue 可以满足这方面的需求,但是性能上并不满足,于是使用了 Disruptor,它是英国外汇交易公司 LMAX 开发的一个高性能队列,了解到它内部解决伪共享问题,今天就和大家一起学习缓存行与伪共享相关的知识。 缓存行(Cache line) 对计算机组成原理相对熟悉的小伙伴都知道,CPU 的速度比内存的速度高了几个数量级,为了 CPU 更快从内存中读取数据,设置了多级缓存机制,如下图所示: 当 CPU 运算时,首先会从 L1 缓存查找所需要的数据,如果没有找到,再去 L2 缓存中去找,以此类推,直到从内存中获取数据,这也就意味着,越长的调用链,所耗费的执行时间也越长。那是不是可以从主内存拿数据的时候,顺便多拿一些呢?这样就可以避免频繁从主内存中获取数据了。聪明的计算机科学家已经想到了这个法子,这就是缓存行的由来。缓存是由多个缓存行组成的,而每个缓存行大小通常来说,大小为 64 字节,并且每个缓存行有效地引用主内存中的一块儿地址,CPU 每次从主内存中获取数据时,会将相邻的数据也一同拉取到缓存行中,这样当 CPU 执行运算时,就大大减少了与主内存的交互。 下面我用一个例子让大家体会一下用缓存行和不用缓存行在性能上的差异: //以下源码例子来源:https://tech.meituan.com/2016/11/18/disruptor.htmlpublicclassCacheLineEffect{//考虑一般缓存行大小是64字节,一个long类型占8字节staticlong[][]arr;publicstaticvoidmain(String[]args){intsize=1024*1024;arr=newlong[size][];for(inti=0;i<size;i++){arr[i]=newlong[8];for(intj=0;j<8;j++){arr[i][j]=0L;}}longsum=0L;longmarked=System.currentTimeMillis();for(inti=0;i<size;i++){for(intj=0;j<8;j++){sum=arr[i][j];}}System.out.println("[cacheline]Looptimes:"+(System.currentTimeMillis()-marked)+"ms");marked=System.currentTimeMillis();for(inti=0;i<8;i+=1){for(intj=0;j<size;j++){sum=arr[j][i];}}System.out.println("[nocacheline]Looptimes:"+(System.currentTimeMillis()-marked)+"ms");}} 我使用的测试运行环境配置如下: 运行后结果如下: 可以看到,使用缓存行比没有使用缓存行的性能提升了将近 4 倍。 伪共享问题 当 CPU 执行完后,还需要将数据回写到内存上,以便于别的线程可以从主内存中获取最新的数据。假设两个线程都加载了相同的 Cache line 数据,会产生什么样的影响呢?下面我用一张图解释: 数据 A、B、C 被加载到同一个 Cache line,假设线程 1 在 core1 中修改 A,线程 2 在 core2 中修改 B。 线程 1 首先对 A 进行修改,这时 core1 会告知其它 CPU 核,当前引用同一地址的 Cache line 已经无效,随后 core2 发起修改 B,会导致 core1 将数据回写到主内存中,core2 这时会重新从主内存中读取该 Cache line 数据。 可见,如果同一个 Cache line 的内容被多个线程读取,就会产生相互竞争,频繁回写主内存,降低了性能。 如何解决伪共享问题 要解决伪共享这个问题最简单的做法就是将线程间共享元素分开到不同的 Cache line 中,这种做法叫用空间换取时间,具体做法如下: publicfinalstaticclassValuePadding{//前置填充对象protectedlongp1,p2,p3,p4,p5,p6,p7;//value值protectedvolatilelongvalue=0L;//后置填充对象protectedlongp9,p10,p11,p12,p13,p14,p15;} JDK1.8 有专门的注解 @Contended 来避免伪共享,为了更加直观,我使用了对象填充的方法,其中 protected long p1, p2, p3, p4, p5, p6, p7 作为前置填充对象,protected long p9, p10, p11, p12, p13, p14, p15作为后置填充对象,这样任意线程访问 ValuePadding 时,value 都处于不同的 Cache line 中,不会产生伪共享问题。 下面的例子用来演示伪共享与解决伪共享后的性能差异: publicclassMyFalseSharing{publicstaticvoidmain(String[]args)throwsInterruptedException{for(inti=1;i<10;i++){System.gc();finallongstart=System.currentTimeMillis();runTest(Type.PADDING,i);System.out.println("[PADDING]Threadnum"+i+"duration="+(System.currentTimeMillis()-start));}for(inti=1;i<10;i++){System.gc();finallongstart=System.currentTimeMillis();runTest(Type.NO_PADDING,i);System.out.println("[NO_PADDING]Threadnum"+i+"duration="+(System.currentTimeMillis()-start));}}privatestaticvoidrunTest(Typetype,intNUM_THREADS)throwsInterruptedException{Thread[]threads=newThread[NUM_THREADS];switch(type){casePADDING:DataPadding.longs=newValuePadding[NUM_THREADS];for(inti=0;i<DataPadding.longs.length;i++){DataPadding.longs[i]=newValuePadding();}break;caseNO_PADDING:Data.longs=newValueNoPadding[NUM_THREADS];for(inti=0;i<Data.longs.length;i++){Data.longs[i]=newValueNoPadding();}break;}for(inti=0;i<threads.length;i++){threads[i]=newThread(newFalseSharing(type,i));}for(Threadt:threads){t.start();}for(Threadt:threads){t.join();}}//线程执行单元staticclassFalseSharingimplementsRunnable{publicfinalstaticlongITERATIONS=500L*1000L*100L;privateintarrayIndex;privateTypetype;publicFalseSharing(Typetype,finalintarrayIndex){this.arrayIndex=arrayIndex;this.type=type;}publicvoidrun(){longi=ITERATIONS+1;//读取共享变量中指定的下标对象,并对其value变量不断修改//由于每次读取数据都会写入缓存行,如果线程间有共享的缓存行数据,就会导致伪共享问题发生//如果对象已填充,那么线程每次读取到缓存行中的对象就不会产生伪共享问题switch(type){caseNO_PADDING:while(0!=--i){Data.longs[arrayIndex].value=0L;}break;casePADDING:while(0!=--i){DataPadding.longs[arrayIndex].value=0L;}break;}}}//线程间贡献的数据publicfinalstaticclassData{publicstaticValueNoPadding[]longs;}publicfinalstaticclassDataPadding{publicstaticValuePadding[]longs;}//使用填充对象publicfinalstaticclassValuePadding{//前置填充对象protectedlongp1,p2,p3,p4,p5,p6;//value值protectedvolatilelongvalue=0L;//后置填充对象protectedlongp9,p10,p11,p12,p13,p14,p15;}//不填充对象//@sun.misc.ContendedpublicfinalstaticclassValueNoPadding{protectedvolatilelongvalue=0L;}enumType{NO_PADDING,PADDING}} 运行程序,测试结果如下: 可见,当有多个线程同时操作同一个 Cache line 的数据时,伪共享问题会影响 CPU 性能。 近期热文 Seata RPC 模块的重构之路 从源码和日志文件结构中分析 Kafka 重启失败事件 记一次 Kafka 重启失败问题排查 图解:Kafka 水印备份机制 记一次 Kafka 集群线上扩容 Kafka重平衡机制 Seata 配置中心实现原理 Seata AT 模式启动源码分析 分布式事务中间件 Seata 的设计原理 我对支付平台架构设计的一些思考 聊聊 Tomcat 的架构设计 关于 Kafka 的一些面试题目 基于Jenkins Pipeline自动化部署 RocketMQ消息发送的高可用设计 深度解析RocketMQ Topic的创建机制 mybatis-plus 源码分析之sql注入器 Mybatis源码分析之Mapper注册与绑定 从源码的角度解析线程池运行原理 关于线程池你不得不知道的一些设置 你都理解创建线程池的参数吗? Java并发之AQS源码分析(二) Java并发之AQS源码分析(一) 本文分享自微信公众号 - 后端进阶(objcoding)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。