原文:False Shareing && Java 7 (依然是马丁的博客) 译者:杨帆 校对:方腾飞
在我前一篇有关伪共享的博文中,我提到了可以加入闲置的long字段来填充缓存行来避免伪共享。但是看起来Java 7变得更加智慧了,它淘汰或者是重新排列了无用的字段,这样我们之前的办法在Java 7下就不奏效了,但是伪共享依然会发生。我在不同的平台上实验了一些列不同的方案,并且最终发现下面的代码是最可靠的。(译者注:下面的是最终版本,马丁在大家的帮助下修改了几次代码)
01 |
import java.util.concurrent.atomic.AtomicLong;
|
03 |
public final class FalseSharing
|
06 |
public final static int NUM_THREADS = 4 ;
|
07 |
public final static long ITERATIONS = 500L * 1000L * 1000L;
|
08 |
private final int arrayIndex;
|
10 |
private static PaddedAtomicLong[] longs = new PaddedAtomicLong[NUM_THREADS];
|
13 |
for ( int i = 0 ; i < longs.length; i++)
|
15 |
longs[i] = new PaddedAtomicLong();
|
19 |
public FalseSharing( final int arrayIndex)
|
21 |
this .arrayIndex = arrayIndex;
|
24 |
public static void main( final String[] args) throws Exception
|
26 |
final long start = System.nanoTime();
|
28 |
System.out.println( "duration = " + (System.nanoTime() - start));
|
31 |
private static void runTest() throws InterruptedException
|
33 |
Thread[] threads = new Thread[NUM_THREADS];
|
35 |
for ( int i = 0 ; i < threads.length; i++)
|
37 |
threads[i] = new Thread( new FalseSharing(i));
|
40 |
for (Thread t : threads)
|
45 |
for (Thread t : threads)
|
53 |
long i = ITERATIONS + 1 ;
|
56 |
longs[arrayIndex].set(i);
|
61 |
public static long sumPaddingToPreventOptimisation( final int index)
|
63 |
PaddedAtomicLong v = longs[index];
|
64 |
return v.p1 + v.p2 + v.p3 + v.p4 + v.p5 + v.p6;
|
67 |
public static class PaddedAtomicLong extends AtomicLong
|
69 |
public volatile long p1, p2, p3, p4, p5, p6 = 7L;
|
用以上这种办法我获得了和上一篇博客里提到的相近的性能,读者可以把PaddedAtomicLong里面那行填充物注释掉再跑测试看看效果。
我想我们大家都有权去跟Oracle投诉,让他们在JDK里默认加入缓存行对齐的函数或者是被填充好的原子类型,这和其他一些底层改变会让Java成为一门真真正正的并发编程语言。我们一直以来不断的在听到他们讲多核时代正在到来,但是我要说的是在这方面Java需要快点赶上来。
———————————————–
(译者注:博文后面的评论和交流也很精彩,也讲述了这段示例代码的进化过程,一起翻译出来:)
1楼:Ashwin Jayaprakash
在前一篇博文中你创建了一个数组来放VolatileLong,这次你又用一个数组放AtomicLongArray(译者注:此处我觉得他可能是写错了,应该是说AtomicLong吧)。
但是如何能保证AtomicLongArray或VolatileLong会被紧挨着分配在内存中?
那么,就算你在一个循环中创建他们,并且很幸运的,他们获得了连续的内存空间,但是依然无法保证这四个实例会在堆空间里紧挨着。如果他们被分布在JVM的旧生代堆里并且没有被压实的话,直到一次主要GC压实旧生代之前,重新分配填充是没必要的,因为他们在堆中是分散的。
所以你最好对读者说明,我们无法控制JVM如何在堆中对这些实例分配内存。(译者注:没办法,要精确控制内存来保证性能的话就不要用Java了,要不直接用C好了)
———————————————–
2楼:马丁
你大体上说的是对的,Ashwin,我们无法保证如何在堆空间中放置Java对象,这是伪共享问题发生的根源。如果你有一些跨线程的指针或者计数器,那么确保他们在不同的缓存行中是非常重要的,否则的话程序就无法按CPU的核数扩展。填充的根本意义在于保护这些跨线程的指针和计数器,以确保他们在不同的缓存行中。
这个是有意义的吧?
———————————————–
3楼:Ashwin Jayaprakash
嗯,有道理。那你可不可以创建一个大的AtomicLongArray,然后让不同的线程去更新第8,16,32个元素呢?(译者注:也算是消除竞争的一个办法,但是既然完全没有竞争还要多线程做什么?)而不是搞四个AtomicLongArray,而每个线程都去竞争访问同一个数组元素。
谢谢马丁花时间写了这么多。
———————————————–
4楼:马丁
如果我可以提前知道更多的业务逻辑那么你说的方式是可行的。但通常情况下在设计一个大型系统的时候,我们无法提前知道很多事情,或者我们要为其他的应用创造一个通用的类库。
我很难为很多不同的上下文场景写一个足够小巧简单的示例,而我上面写的示例是为了说明当伪共享发生的时候有多糟糕。如果你在你的数据结构中做了填充,那么你就不必担心他们在内存中如何分配。我们用一个更好的方案来替代AtomicLong,并且你可以使用AtomicLong的所有常规方法:
static class PaddedAtomicLong extends AtomicLong
{
public volatile long p1, p2, p3, p4, p5, p6, p7 = 7L;
}
我是多希望Java委员会可以认识到这个问题的严重性,并且在JDK里加入对缓存行对齐和填充的基础方法。这是在Disruptor中有关性能BUG的最大根源。
我也根据以上的反馈更新了文章。
———————————————–
5楼:Gil Tene
马丁,我很同意你的观点,如果我们有一种办法可以指定某个字段占有独自的缓存行,并且让JVM自动处理如何在对象布局上的正确填充,那这个世界会和谐的多。你搞的这个人造填充将会是很美好的一个事情,但是你也知道,实际上的对象布局情况要取决于JVM的特定实现。
我是一个偏执狂,我给你的填充方案里加了一些东西,使那些个用于填充的字段很难被JVM优化掉。一个耍小聪明的JVM还是会把你用于填充的P1-P7的字段优化掉,原理是这样滴:PaddedAtomicLong类如果只对final的FalseSharing类可见(就是说PaddedAtomicLong不能再被继承了)。这样一来编译器就会“知道”它正在审视的是所有可以看到这个填充字段的代码,这样就可以证明没有行为依赖于p1到p7这些字段。那么“聪明”的JVM会把上面这些丝毫不占地方的字段统统优化掉。
那么针对这样的情况,你可以巧妙的让PaddedAtomicLong类在FalseSharing类之外可见,比如直接加一个依赖于p1到p7的公开的访问函数,并且这个函数在理论上可以被外界访问到。
———————————————–
6楼:马丁
我根据Gil的反馈做了修改。
———————————————–
7楼:Stanimir Simeonoff
直接用一个数组并且把元素放在中间的位置上(或者直接用bytebuffer,而你却为了这个写了这么一大篇),Java是不会重排他们的,我就是这样来消除伪共享的。
———————————————–
8楼:马丁
我以前经常像你这么干,比如搞一个长度是15的数组,把元素放在正中间,但是,如果我们需要volatile这个语意就行不通了。对于你的情况来说,你只需要用AtomicLongArray或者类似的。根据我的测量,在一个算法中,这种间接引用(译者注:原词是indirection,我理解也许是指间接引用,即不是直接使用数组,而是使用AtomicLongArray这种包装过的数组)和边界检查的消耗是显著的。
据我所知,一些人建议加入@Contened注解来标记一个字段,让这个被标记的字段拥有独立的缓存行,我希望这个快点到来。
8.1楼:John
你好,马丁,我看到在Disruptor当前的版本中Sequence类用的是unsafe.compareAndSwapLong(..)来更新第七个下标的long。
为什么不数组的长度不是15或者是其他的数值?如果长度是15的话会把2级缓存的缓存行也填充掉么?
谢谢。
8.2楼:马丁
因为用7个下标保证了会有56个字节填充在数值的任何一边,56字节的填充+8字节的long数值正好装进一行64字节的缓存行。
———————————————–
9楼:Stanimir Simeonoff
是的,马丁,我指的就是AtomicLongArray,如果你不想为间接引用和边界检查买单,Unsafe 是一个选项(甚至总是这样)。
———————————————–
10楼:Mohan Radhakrishnan
哪里有一些简单硬件说明书是讲述缓存行的关键概念么?我想找一些插图什么的来理解核心和缓存直接如何交互造成了伪共享。
———————————————–
11楼:马丁
你可以参照下面这个PDF的第3和第4章:
http://img.delivery.net/cm50content/intel/ProductLibrary/100412_Parallel_Programming_02.pdf
———————————————–
12楼:ying
你的博客太NB了,多谢马丁,我关于填充有两个疑问:
1.long占8字节,对象引用占16字节,但是这个实现是
1 |
public final static class VolatileLong
|
3 |
public volatile long value = 0L;
|
4 |
public long p1, p2, p3, p4, p5, p6;
|
看起来好像是72个字节啊。
2.你是发现这个问题的?是去查汇编代码吗?
12.1楼:马丁
我不希望在缓存行中的标记字在取出锁或者垃圾回收器在老化对象的时候被修改。
就算默认启用64位模式的压缩指针,它还是会包含类指针在对象的头部。
https://wikis.oracle.com/display/HotSpotInternals/CompressedOops
这个伪共享的问题我是在多年前发现的,但是我为一个应用做性能测试,发现性能时高时低,追查原因下去发现是伪共享问题。
12.2楼:ying
那你是如何缩小问题的范围最后发现问题的呢?需要深入分析汇编代码么?
11.3楼:马丁
汇编代码是不会显示出问题的,你需要去追查为什么CPU的2级缓存总是不命中,追查下去就知道了。
———————————————–
13楼:Joachim
关于@Contended注解的提案在这里:
http://mail.openjdk.java.net/pipermail/hotspot-dev/2012-November/007309.html
牛文啊,赞!
相关推荐
<Call Stack = DEBUG_FRAME = org.apache.axis2.util.JavaUtils.callStackToString(JavaUtils.java:564) DEBUG_FRAME = org.apache.axis2.description.ParameterIncludeImpl.debugParameterAdd(ParameterIncludeImpl...
Android DiskLruCache的源码
java.applet java.awt java.awt.color java.awt.datatransfer java.awt.dnd java.awt.event java.awt.font java.awt.geom java.awt.im java.awt.im.spi java.awt.image java.awt.image.renderable java....
二级java 公共基础知识部分30分 专业语言部分 70分 Java语言程序设计 基本要求: 1. 掌握Java语言的特点,实现机制和体系结构。 2. 掌握Java语言中面向对象的特性。 3. 掌握Java语言提供的数据类型和结构。 4. 掌握...
这是一本以面试题为入口讲解 Java 核心内容的技术书籍,书中内容极力的向你证实代码是对数学逻辑的具体实现。当你仔细阅读书籍时,会发现Java中有大量的数学知识,包括:扰动函数、负载因子、拉链寻址、开放寻址、...
3. **字符串拼接优化**:Java 7改进了字符串拼接,当使用`+`操作符连接字符串时,会使用StringBuilder或StringBuffer的append方法,提高了性能。 4. **文件系统API**:NIO.2引入了一套全新的文件系统API,提供了...
使用smbj.jar访问共享文件夹,支持SMB2/SMB3,用于解决使用jcifs.jar不支持SMB2/SMB3的问题
Md5Util.java
这个工具不仅小巧,而且功能强大,能够100%地将.class文件还原为.java文件,尽管有时生成的源代码可能与原始的源代码略有不同,因为反编译过程中可能会丢失一些元数据和注解。 在Java世界里,.class文件是由Java...
java.awt.geom 提供用于在与二维几何形状相关的对象上定义和执行操作的 Java 2D 类。 java.awt.im 提供输入方法框架所需的类和接口。 java.awt.im.spi 提供启用可以与 Java 运行时环境一起使用的输入方法开发的...
在本项目中,"日历备忘录Java源码" 提供了一套完整的日历应用程序的源代码,主要由四个核心文件组成:NotePad.java、CalendarPad.java、Month.java 和 Year.java。这些文件分别代表了备忘录、日历、月份和年份的功能...
这个特定的项目是基于JAVA实现的屏幕共享程序,它提供了在局域网内进行屏幕共享的功能。以下是对该程序及其相关技术的详细解释: 首先,让我们关注核心编程语言——JAVA。JAVA是一种广泛使用的面向对象的编程语言,...
1、下载WSDL2JAVA.rar包,其中包含activation.jar,axis-ant.jar,axis.jar,commons- discovery-0.2.jar,commons-logging-1.0.4.jar,jaxrpc.jar,log4j- 1.2.8.jar,mail.jar,saaj.jar,wsdl4j-1.5.1.jar。...
ERROR org.apache.hadoop.mapred.TaskTracker: Can not start task tracker because java.io.IOException: Failed to set permissions of path: \tmp\hadoop-admin \mapred\local\ttprivate to 0700 at org.apache...
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class Wav2mp3 { public static void main(String[] args) { String wavFilePath = "path_to_your.wav";...
java7 32位,一个不错的插件,提供给那些需要的朋友们。呵呵。
java.awt.geom 提供用于在与二维几何形状相关的对象上定义和执行操作的 Java 2D 类。 java.awt.im 提供输入方法框架所需的类和接口。 java.awt.im.spi 提供启用可以与 Java 运行时环境一起使用的输入方法开发的接口...
第二章 Java程序开发与运行环境 第三章 Java程序设计基础 第四章 Java应用程序的基本框架 第五章 Java的类 第六章 Java图形用户接口 第七章 多线程 第八章 Java的"异常" 第九章 Java输入输出操作 java新手...
解决Eclipse中使用drool时报Caused by: java.lang.RuntimeException: The Eclipse JDT Core jar is not in the classpath的问题。 详细错误: org.drools.RuntimeDroolsException: Unable to load dialect 'org....
Java JDK 11.0.8 是Oracle公司发布的Java开发工具包的一个稳定版本,它针对开发者提供了完整的编译、调试和运行Java应用程序所需的环境。这个版本支持Windows和Mac OS操作系统,使得不同平台上的开发者都能方便地...