原文地址:http://ifeve.com/false-sharing/
作者:Martin Thompson  译者:丁一
缓存系统中是以缓存行(cache line)为单位存储的。缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。缓存行上的写竞争是运行在SMP系统中并行线程实现可伸缩性最重要的限制因素。有人将伪共享描述成无声的性能杀手,因为从代码中很难看清楚是否会出现伪共享。
为了让可伸缩性与线程数呈线性关系,就必须确保不会有两个线程往同一个变量或缓存行中写。两个线程写同一个变量可以在代码中发现。为了确定互相独立的变量是否共享了同一个缓存行,就需要了解内存布局,或找个工具告诉我们。Intel VTune就是这样一个分析工具。本文中我将解释Java对象的内存布局以及我们该如何填充缓存行以避免伪共享。
  | 
| 图 1. | 
图1说明了伪共享的问题。在核心1上运行的线程想更新变量X,同时核心2上的线程想要更新变量Y。不幸的是,这两个变量在同一个缓存行中。每个线程都要去竞争缓存行的所有权来更新变量。如果核心1获得了所有权,缓存子系统将会使核心2中对应的缓存行失效。当核心2获得了所有权然后执行更新操作,核心1就要使自己对应的缓存行失效。这会来来回回的经过L3缓存,大大影响了性能。如果互相竞争的核心位于不同的插槽,就要额外横跨插槽连接,问题可能更加严重。
Java内存布局(Java Memory Layout)
对于HotSpot JVM,所有对象都有两个字长的对象头。第一个字是由24位哈希码和8位标志位(如锁的状态或作为锁对象)组成的Mark Word。第二个字是对象所属类的引用。如果是数组对象还需要一个额外的字来存储数组的长度。每个对象的起始地址都对齐于8字节以提高性能。因此当封装对象的时候为了高效率,对象字段声明的顺序会被重排序成下列基于字节大小的顺序:
- doubles (8) 和 longs (8)
 
- ints (4) 和 floats (4)
 
- shorts (2) 和 chars (2)
 
- booleans (1) 和 bytes (1)
 
- references (4/8)
 
- <子类字段重复上述顺序>
 
(译注:更多HotSpot虚拟机对象结构相关内容:http://www.infoq.com/cn/articles/jvm-hotspot)
了解这些之后就可以在任意字段间用7个long来填充缓存行。在Disruptor里我们对RingBuffer的cursor和BatchEventProcessor的序列进行了缓存行填充。
为了展示其性能影响,我们启动几个线程,每个都更新它自己独立的计数器。计数器是volatile long类型的,所以其它线程能看到它们的进展。
01 | 
public final class FalseSharing | 
 
04 | 
    public final static int NUM_THREADS = 4; // change | 
 
05 | 
    public final static long ITERATIONS = 500L * 1000L * 1000L; | 
 
06 | 
    private final int arrayIndex; | 
 
08 | 
    private static VolatileLong[] longs = new VolatileLong[NUM_THREADS]; | 
 
11 | 
        for (int i = 0; i < longs.length; i++) | 
 
13 | 
            longs[i] = new VolatileLong(); | 
 
17 | 
    public FalseSharing(final int arrayIndex) | 
 
19 | 
        this.arrayIndex = arrayIndex; | 
 
22 | 
    public static void main(final String[] args) throws Exception | 
 
24 | 
        final long start = System.nanoTime(); | 
 
26 | 
        System.out.println("duration = " + (System.nanoTime() - start)); | 
 
29 | 
    private static void runTest() throws InterruptedException | 
 
31 | 
        Thread[] threads = new Thread[NUM_THREADS]; | 
 
33 | 
        for (int i = 0; i < threads.length; i++) | 
 
35 | 
            threads[i] = new Thread(new FalseSharing(i)); | 
 
38 | 
        for (Thread t : threads) | 
 
43 | 
        for (Thread t : threads) | 
 
51 | 
        long i = ITERATIONS + 1; | 
 
54 | 
            longs[arrayIndex].value = i; | 
 
58 | 
    public final static class VolatileLong | 
 
60 | 
        public volatile long value = 0L; | 
 
61 | 
        public long p1, p2, p3, p4, p5, p6; // comment out | 
 
 
 
结果(Results)
运行上面的代码,增加线程数以及添加/移除缓存行的填充,下面的图2描述了我得到的结果。这是在我4核Nehalem上测得的运行时间。
  | 
| 图 2. | 
从不断上升的测试所需时间中能够明显看出伪共享的影响。没有缓存行竞争时,我们几近达到了随着线程数的线性扩展。
这并不是个完美的测试,因为我们不能确定这些VolatileLong会布局在内存的什么位置。它们是独立的对象。但是经验告诉我们同一时间分配的对象趋向集中于一块。
所以你也看到了,伪共享可能是无声的性能杀手。
解决方法:
我们知道一条缓存行有64字节, 而Java程序的对象头固定占8字节(32位系统)或12字节(64位系统默认开启压缩, 不开压缩为16字节). 我们只需要填6个无用的长整型补上6*8=48字节, 让不同的VolatileLong对象处于不同的缓存行, 就可以避免伪共享了(64位系统超过缓存行的64字节也无所谓,只要保证不同线程不要操作同一缓存行就可以)。这个办法叫做补齐(Padding)。
Java8中的解决方案
Java8中已经提供了官方的解决方案,Java8中新增了一个注解:@sun.misc.Contended。加上这个注解的类会自动补齐缓存行,需要注意的是此注解默认是无效的,需要在jvm启动时设置-XX:-RestrictContended才会生效。
 												
								- Java 中的伪共享详解及解决方案
		
1. 什么是伪共享 CPU 缓存系统中是以缓存行(cache line)为单位存储的.目前主流的 CPU Cache 的 Cache Line 大小都是 64 Bytes.在多线程情况下,如果需要修改 ...
		 
						- Java中共享设计
		
Java中的共享设计的思路是在Java中形成一个对象池,在这个对象池中保存多个对象, 新实例化的对象如果已经在池中定义了,则不再重复新定义,而从池中直接取出继续使用. 例如,对于字符串来说,Java  ...
		 
						- 从零开始实现lmax-Disruptor队列(六)Disruptor 解决伪共享、消费者优雅停止实现原理解析
		
MyDisruptor V6版本介绍 在v5版本的MyDisruptor实现DSL风格的API后.按照计划,v6版本的MyDisruptor作为最后一个版本,需要对MyDisruptor进行最终的一些 ...
		 
						- Java中String、StringBuffer、StringBuilder区别与理解
		
一.先比较String.StringBuffer.StringBuilder变量的HashCode值 使用System.out.println(obj.hashcode())输出的时对象的哈希码, 而 ...
		 
						- 关于java中的伪共享的认识和解决
		
在并发编程过程中,我们大部分的焦点都放在如何控制共享变量的访问控制上(代码层面),但是很少人会关注系统硬件及 JVM 底层相关的影响因素: CPU缓存 网页浏览器为了加快速度,会在本机存缓存以前浏览过 ...
		 
						- 多线程中的volatile和伪共享
		
  伪共享 false sharing,顾名思义,“伪共享”就是“其实不是共享”.那什么是“共享”?多CPU同时访问同一块内存区域就是“共享”,就会产生冲突,需要控制协议来协调访问.会引起“共享”的最 ...
		 
						- 伪共享和缓存行填充,从Java 6, Java 7 到Java 8
		
关于伪共享的文章已经很多了,对于多线程编程来说,特别是多线程处理列表和数组的时候,要非常注意伪共享的问题.否则不仅无法发挥多线程的优势,还可能比单线程性能还差.随着JAVA版本的更新,再各个版本上减少 ...
		 
						- java 伪共享
		
MESI协议及RFO请求典型的CPU微架构有3级缓存, 每个核都有自己私有的L1, L2缓存. 那么多线程编程时, 另外一个核的线程想要访问当前核内L1, L2 缓存行的数据, 该怎么办呢?有人说可以 ...
		 
						- 从Java视角理解CPU缓存和伪共享
		
转载自:http://ifeve.com/from-javaeye-cpu-cache/               http://ifeve.com/from-javaeye-false-shari ...
		 
		
	
									- [android] 服务的生命周期(混合方式)
			
绑定服务:可以调用服务里面的方法, 如果调用者activity销毁了,服务也会跟着销毁 单独解除绑定的时候,服务也会被销毁 开启服务:不可以调用服务里面的方法 如果调用者activity退出了,服务还 ...
			 
						- Eclipse中提示svn: is already locked的解决办法
			
eclipse的svn提交不了,报错.提示 svn: is already locked   解决办法:右键项目-------Team------Refresh/Cleanup
			 
						- Reinforcement Learning: An Introduction读书笔记(2)--多臂机
			
 > 目  录 <  k-armed bandit problem Incremental Implementation Tracking a Nonstationary Problem  ...
			 
						- js 每到达5次换一行
			
function getYourString(s) { var res = ''; var length = s.length; for (var i = 0, j = 1; i < lengt ...
			 
						- es6 语法 (正则扩展)
			
{ //es5中 let regex = new RegExp('xyz', 'i'); let regex2 = new RegExp(/xyz/i); console.log(regex.test ...
			 
						- 关于select 文字居向
			
我们都知道select的文字默认居左,而如果你想改变它,用text-align是不起作用的,因为select没有这个样式 但是它有自己的样式属性 文字靠右对齐:direction: rtl; 而如果要 ...
			 
						- 接口自动化 [授客]基于python+Testlink+Jenkins实现的接口自动化测试框架V3.0
			
基于python+Testlink+Jenkins实现的接口自动化测试框架V3.0   by:授客 QQ:1033553122     博客:http://blog.sina.com.cn/ishou ...
			 
						- 解决VS2015单元测试“未能设置用于运行测试的执行上下文”问题
			
VS的单元测试在进行测试时并不像普通Exe会为你提示xx文件未找到,而是类似下面这样: 测试名称: 部署文件到Linux测试全名: unittest::SmartDispatch::部署文件到Linu ...
			 
						- matlab练习程序(曲面拟合)
			
这里用到的还是最小二乘方法,和上一次这篇文章原理差不多. 就是首先构造最小二乘函数,然后对每一个系数计算偏导,构造矩阵乘法形式,最后解方程组. 比如有一个二次曲面:z=ax^2+by^2+cxy+dx ...
			 
						- tkinter进阶版——ttk
			
很长的一段时间里,我都是用tkinter进行GUI设计的,还写过一篇<tkinter模块常用参数>. 但后来慢慢地觉得,这个tkinter真的是有点丑啊. 于是,找到了现在的ttk. tt ...