伪共享和缓存行填充,Java并发编程还能这么优化!

前言

关于伪共享的文章已经很多了,对于多线程编程来说,特别是多线程处理列表和数组的时候,要非常注意伪共享的问题。否则不仅无法发挥多线程的优势,还可能比单线程性能还差。随着JAVA版本的更新,再各个版本上减少伪共享的做法都有区别,一不小心代码可能就失效了,要注意进行测试。这篇文章总结一下。

什么是伪共享
关于伪共享讲解最清楚的是这篇文章:http://developer.51cto.com/art/201306/398232.htm,我这里就直接摘抄其对伪共享的解释:

缓存系统中是以缓存行(cache line)为单位存储的。缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。缓存行上的写竞争是运行在SMP系统中并行线程实现可伸缩性最重要的限制因素。有人将伪共享描述成无声的性能杀手,因为从代码中很难看清楚是否会出现伪共享。

为了让可伸缩性与线程数呈线性关系,就必须确保不会有两个线程往同一个变量或缓存行中写。两个线程写同一个变量可以在代码中发现。为了确定互相独立的变量是否共享了同一个缓存行,就需要了解内存布局,或找个工具告诉我们。Intel VTune就是这样一个分析工具。本文中我将解释Java对象的内存布局以及我们该如何填充缓存行以避免伪共享。

image


图1说明了伪共享的问题。在核心1上运行的线程想更新变量X,同时核心2上的线程想要更新变量Y。不幸的是,这两个变量在同一个缓存行中。每个线程都要去竞争缓存行的所有权来更新变量。如果核心1获得了所有权,缓存子系统将会使核心2中对应的缓存行失效。当核心2获得了所有权然后执行更新操作,核心1就要使自己对应的缓存行失效。这会来来回回的经过L3缓存,大大影响了性能。如果互相竞争的核心位于不同的插槽,就要额外横跨插槽连接,问题可能更加严重。

JAVA 6下的方案

解决伪共享的办法是使用缓存行填充,使一个对象占用的内存大小刚好为64bytes或它的整数倍,这样就保证了一个缓存行里不会有多个对象。这篇文章http://developer.51cto.com/art/201306/398232.htm提供了缓存行填充的例子:

public final class FalseSharing implements Runnable 
{ public final static int NUM_THREADS = 4; // change public final static long ITERATIONS = 500L * 1000L * 1000L; private final int arrayIndex; private static VolatileLong[] longs = new VolatileLong[NUM_THREADS]; static { for (int i = 0; i < longs.length; i++) { longs[i] = new VolatileLong(); } } public FalseSharing(final int arrayIndex) { this.arrayIndex = arrayIndex; } public static void main(final String[] args) throws Exception { final long start = System.nanoTime(); runTest(); System.out.println("duration = " + (System.nanoTime() - start)); } private static void runTest() throws InterruptedException { Thread[] threads = new Thread[NUM_THREADS]; for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(new FalseSharing(i)); } for (Thread t : threads) { t.start(); } for (Thread t : threads) { t.join(); } } public void run() { long i = ITERATIONS + 1; while (0 != --i) { longs[arrayIndex].value = i; } } public final static class VolatileLong { public volatile long value = 0L; public long p1, p2, p3, p4, p5, p6; // comment out } 
}

VolatileLong通过填充一些无用的字段p1,p2,p3,p4,p5,p6,再考虑到对象头也占用8bit, 刚好把对象占用的内存扩展到刚好占64bytes(或者64bytes的整数倍)。这样就避免了一个缓存行中加载多个对象。但这个方法现在只能适应JAVA6 及以前的版本了。

(注:如果我们的填充使对象size大于64bytes,比如多填充16bytes– public long p1, p2, p3, p4, p5, p6, p7, p8;。理论上同样应该避免伪共享问题,但事实是这样的话执行速度同样慢几倍,只比没有使用填充好一些而已。还没有理解其原因。所以测试下来,必须是64bytes的整数倍)

JAVA 7下的方案
上面这个例子在JAVA 7下已经不适用了。因为JAVA 7会优化掉无用的字段,可以参考:http://ifeve.com/false-shareing-java-7-cn/。

因此,JAVA 7下做缓存行填充更麻烦了,需要使用继承的办法来避免填充被优化掉,这篇文章http://ifeve.com/false-shareing-java-7-cn/里的例子我觉得不是很好,于是我自己做了一些优化,使其更通用:

public final class FalseSharing implements Runnable {  public static int NUM_THREADS = 4; // change  public final static long ITERATIONS = 500L * 1000L * 1000L;  private final int arrayIndex;  private static VolatileLong[] longs;  public FalseSharing(final int arrayIndex) {  this.arrayIndex = arrayIndex;  }  public static void main(final String[] args) throws Exception {  Thread.sleep(10000);  System.out.println("starting....");  if (args.length == 1) {  NUM_THREADS = Integer.parseInt(args[0]);  }  longs = new VolatileLong[NUM_THREADS];  for (int i = 0; i < longs.length; i++) {  longs[i] = new VolatileLong();  }  final long start = System.nanoTime();  runTest();  System.out.println("duration = " + (System.nanoTime() - start));  }  private static void runTest() throws InterruptedException {  Thread[] threads = new Thread[NUM_THREADS];  for (int i = 0; i < threads.length; i++) {  threads[i] = new Thread(new FalseSharing(i));  }  for (Thread t : threads) {  t.start();  }  for (Thread t : threads) {  t.join();  }  }  public void run() {  long i = ITERATIONS + 1;  while (0 != --i) {  longs[arrayIndex].value = i;  }  }  
}public class VolatileLongPadding {public volatile long p1, p2, p3, p4, p5, p6; // 注释  
}public class VolatileLong extends VolatileLongPadding {public volatile long value = 0L;  
}

把padding放在基类里面,可以避免优化。(这好像没有什么道理好讲的,JAVA7的内存优化算法问题,能绕则绕)。不过,这种办法怎么看都有点烦,借用另外一个博主的话:做个java程序员真难。

JAVA 8下的方案
在JAVA 8中,缓存行填充终于被JAVA原生支持了。JAVA 8中添加了一个@Contended的注解,添加这个的注解,将会在自动进行缓存行填充。以上的例子可以改为:

public final class FalseSharing implements Runnable {  public static int NUM_THREADS = 4; // change  public final static long ITERATIONS = 500L * 1000L * 1000L;  private final int arrayIndex;  private static VolatileLong[] longs;  public FalseSharing(final int arrayIndex) {  this.arrayIndex = arrayIndex;  }  public static void main(final String[] args) throws Exception {  Thread.sleep(10000);  System.out.println("starting....");  if (args.length == 1) {  NUM_THREADS = Integer.parseInt(args[0]);  }  longs = new VolatileLong[NUM_THREADS];  for (int i = 0; i < longs.length; i++) {  longs[i] = new VolatileLong();  }  final long start = System.nanoTime();  runTest();  System.out.println("duration = " + (System.nanoTime() - start));  }  private static void runTest() throws InterruptedException {  Thread[] threads = new Thread[NUM_THREADS];  for (int i = 0; i < threads.length; i++) {  threads[i] = new Thread(new FalseSharing(i));  }  for (Thread t : threads) {  t.start();  }  for (Thread t : threads) {  t.join();  }  }  public void run() {  long i = ITERATIONS + 1;  while (0 != --i) {  longs[arrayIndex].value = i;  }  }  
}@Contended
public class VolatileLong {public volatile long value = 0L;  
}

执行时,必须加上虚拟机参数-XX:-RestrictContended,@Contended注释才会生效。很多文章把这个漏掉了,那样的话实际上就没有起作用。

@Contended注释还可以添加在字段上,今后再写文章详细介绍它的用法。

(后记:以上代码基于32位JDK测试,64位JDK下,对象头大小不同,有空再测试一下)

参考

http://mechanical-sympathy.blogspot.com/2011/07/false-sharing.html

http://mechanical-sympathy.blogspot.hk/2011/08/false-sharing-java-7.html

http://robsjava.blogspot.com/2014/03/what-is-false-sharing.html

原文发布时间为:2018-07-12
本文作者:Binhua
本文来自云栖社区合作伙伴“Java架构沉思录”,了解相关信息可以关注“Java架构沉思录”。

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

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

相关文章

mysql中like % %模糊查询

1&#xff0c;%&#xff1a;表示任意0个或多个字符。可匹配任意类型和长度的字符&#xff0c;有些情况下若是中文&#xff0c;请使用两个百分号&#xff08;%%&#xff09;表示。 比如 SELECT * FROM [user] WHERE u_name LIKE %三% 将会把u_name为“张三”&#xff0c;“张猫三…

Java中判断字符串是否为数字的五种方法

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 推荐使用第二个方法&#xff0c;速度最快。 方法一&#xff1a;用JAVA自带的函数 Java代码 public static boolean isNumeric(String…

慕学在线网0.4_xadmin后台管理

admin是基于Django开发的后台管理框架&#xff0c;方便&#xff0c;快捷&#xff0c;而且简单&#xff1b;   而xadmin就相当于admin的升级版&#xff0c;更加强大。    1、安装xadmin&#xff08;源码安装方式&#xff09;  教程 PS&#xff1a; - 卸载pip安装的xadminp…

从一次换机器的过程谈软硬件的分离

今天把在公司使用的计算机更换了一台&#xff0c;原来是Dell的780&#xff0c;换成了Dell的790&#xff0c;机箱的样子变化比较大&#xff0c;但是里面硬件的配置变换并不大&#xff0c;最明显的变化就在于CPU&#xff0c;其他像内存、硬盘等等的配置与原来的计算机基本上一致。…

知其所以然~redis的原子性

原子性 原子性是数据库的事务中的特性。在数据库事务的情景下&#xff0c;原子性指的是&#xff1a;一个事务&#xff08;transaction&#xff09;中的所有操作&#xff0c;要么全部完成&#xff0c;要么全部不完成&#xff0c;不会结束在中间某个环节。 对于Redis而言&#xf…

JoinPoint的用法

JoinPoint 对象 JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象. 常用api: 方法名功能Signature getSignature();获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息…

解决 No projects are available for deployment to this server!

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 如题&#xff0c;今天在尝试部署从SVN上down下来的项目时&#xff0c;发现不能被tomcat识别成web项目&#xff01;原因是SVN上down下来的…

地大信工成果快报

在成果快报投稿中&#xff0c;请注意以下几个问题&#xff1a;&#xff08;1&#xff09;成果信息一定要准确、全面&#xff0c;所有作者必须都要列出来&#xff0c;而不要出现et al. 这样的表述&#xff0c;通讯作者一定要用*号标注&#xff1b;&#xff08;2&#xff09;成果…

javaBean的命名规则

前段时间&#xff0c;写程序时&#xff0c;出了错误&#xff0c;竟然没有想到是自己属性命名的问题&#xff0c;哎~~~真是一定要注意规范呀&#xff0c;在这里我从网上找了些&#xff0c;规范作为参考 Sun 推荐的命名规范 1 &#xff0c;类名要首字母大写&#xff0c;后面的单词…

volatile的应用

volatile&#xff0c;中文意思是不稳定的、反复无常的&#xff0c;用来修饰变量&#xff0c;和多线程、并发有关系。 Java代码在编译后会变成Java字节码&#xff0c;字节码被类加载器加载到JVM里&#xff0c;JVM执行字节码&#xff0c;最终需要转化为汇编指令在CPU上执行。 在多…

漫谈国内智能手机市场现状

本文纯属一时兴起&#xff0c;想到哪儿写到哪儿&#xff0c;本人文笔也不咋地&#xff0c;写的也比较随意&#xff0c;如有错误欢迎指正&#xff0c;有啥意见欢迎交流。原创文章&#xff0c;转载注明emouse的技术专栏。 我是一个不折不扣的数码爱好者&#xff0c;对电脑手机这些…

【刷题】BZOJ 4195 [Noi2015]程序自动分析

Description 在实现程序自动分析的过程中,常常需要判定一些约束条件是否能被同时满足。 考虑一个约束满足问题的简化版本&#xff1a;假设x1,x2,x3,…代表程序中出现的变量&#xff0c;给定n个形如xixj或xi≠xj的变量相等/不等的约束条件&#xff0c;请判定是否可以分别为每一个…

mysql 5.5 安装配置方法图文教程

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 回忆一下mysql 5.5 安装配置方法&#xff0c;整理mysql 5.5 安装配置教程笔记&#xff0c;分享给大家。 MySQL下载地址&#xff1a;htt…

git解除与远程分支的关联

在工作中&#xff0c;经常需要将同一份代码传到不同的git仓库中去 如果本地同样一份代码&#xff0c;已经关联了一个与远程分支&#xff0c;那么怎么才能解除原程分支&#xff0c;并关联到一个新的分支将代码提交到新的分支上去呢&#xff1f; 1、如果你已经在远程创建了一个分…

FindWindow用法

函数功能&#xff1a;该函数获得一个顶层窗口的句柄&#xff0c;该窗口的类名和窗口名与给定的字符串相匹配。这个函数不查找子窗口。在查找时不区分大小写。 函数型&#xff1a;HWND FindWindow&#xff08;LPCTSTR IpClassName&#xff0c;LPCTSTR IpWindowName&#xff0…

中国大城市政治地位综合实力排名

中国大城市政治地位综合实力排名&#xff01; 中国大城市政治地位综合实力排名&#xff01;政治地位: 政治地位: 1&#xff08;直辖市 4 个&#xff09;&#xff1a;上海、北京、天津、重庆 2&#xff08;副省级城市 15 个&#xff09;&#xff1a;广州、深圳、武汉、南京、沈阳…

sourcemap总结

sourcemap在线上压缩文件调试中很重要&#xff0c;在此总结如下&#xff1a; 1. 开启sourcemap (1). 浏览器要开启source-map支持(2). 压缩文件底部要有source-map的URL&#xff0c;压缩要开启source-map(3). .map文件要放在服务器&#xff0c;source-map URL指向的位置 2. sou…

navicat 导出的sql文件,再导入,运行SQL文件成功,数据库中却没有表

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 问题描述&#xff1a;本来在数据库上右键 &#xff0c;运行SQL文件 &#xff0c;就可以导入 sql ,建表成功&#xff0c;并且数据也该的…

mysql索引之二级索引学习总结

二级索引又称辅助索引、非聚集索引(no-clustered index)。b&#xff0b;tree树结构。然而二级索引的叶子节点不保存记录中的所有列&#xff0c;其叶子节点保存的是<健值&#xff0c;(记录)地址>。好似聚集索引中非叶子节点保存的信息&#xff0c;不同的是二级索引保存的是…

264,avs中Skip宏块与Direct预测模式 ,对称模式的区别

1. B_Skip类型宏块 &#xff1a;无像素残差&#xff0c;无运动矢量残差&#xff08;MVD&#xff09;和参考帧。解码时&#xff0c;通过Direct预测模式&#xff08;时间或空间&#xff09;计算出前、后向MV后&#xff0c;直接利用前、后向MV得到像素预测值。像 素重构值像…