今天在学习并发编程时看到一段很有意思的代码,代码如下:
/** 队列中的头部节点 */
private transient f?inal PaddedAtomicReference<QNode> head;
/** 队列中的尾部节点 */
private transient f?inal PaddedAtomicReference<QNode> tail;
static f?inal class PaddedAtomicReference <T> extends AtomicReference T> {
// 使用很多4个字节的引用追加到64个字节
Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe;
PaddedAtomicReference(T r) {
super(r);
}
}
public class AtomicReference <V> implements java.io.Serializable {
private volatile V value;
// 省略其他代码
}
这串代码是由著名的Java并发编程大师Doug lea在JDK 7的并发包里新增一个队列集合类LinkedTransferQueue,它在使用volatile变量时,用一种追加字节的方式来优化队列出队和入队的性能。
当然,以上都不是今天学习的重点,今天的重点是cup伪共享!
-------------------------------------一下内容均为摘录-----------------------------------------------
认识CPU Cache
CPU Cache概述
随着CPU的频率不断提升,而内存的访问速度却没有质的突破,为了弥补访问内存的速度慢,充分发挥CPU的计算资源,提高CPU整体吞吐量,在CPU与内存之间引入了一级Cache。随着热点数据体积越来越大,一级Cache L1已经不满足发展的要求,引入了二级Cache L2,三级Cache L3。(注:若无特别说明,本文的Cache指CPU Cache,高速缓存)CPU Cache在存储器层次结构中的示意如下图:
计算机早已进入多核时代,软件也越来越多的支持多核运行。一个处理器对应一个物理插槽,多处理器间通过QPI总线相连。一个处理器包含多个核,一个处理器间的多核共享L3 Cache。一个核包含寄存器、L1 Cache、L2 Cache,下图是Intel Sandy Bridge CPU架构,一个典型的NUMA多处理器结构:
我们常见的X86芯片:整个Cache被分为S个组,每个组是又由E行个最小的存储单元——Cache Line所组成,而一个Cache Line中有B(B=64)个字节用来存储数据,即每个Cache Line能存储64个字节的数据,每个Cache Line又额外包含一个有效位(valid bit)、t个标记位(tag bit),其中valid bit用来表示该缓存行是否有效;tag bit用来协助寻址,唯一标识存储在CacheLine中的块;而Cache Line里的64个字节其实是对应内存地址中的数据拷贝。根据Cache的结构题,我们可以推算出每一级Cache的大小为B×E×S。
-------------------------------------一上内容均为摘录-----------------------------------------------
Cache Line伪共享及解决方案
Cache Line伪共享分析
说伪共享前,先看看Cache Line 在java编程中使用的场景。如果CPU访问的内存数据不在Cache中(一级、二级、三级),这就产生了Cache Line miss问题,这是,cup又会发送新的指令,从内存中加载数据,越是存储层次靠后的数据,越是消耗性能。故,为提高性能,我们不得不提高缓存的命中率:
在java虚拟机规范中,局部变量区等价于一个数组,并且可以用正整数来索引。除了long,double值需要两个数组单元来存储之外,其他基本数据类型均用一个数组单元;boolean,byte,char,short这四种类型,在栈上占用的空间和int一样,和引用类型也是一样。因此,在32位hotspot中,这些类型再栈上占用4个字节;而在64位hotspot中,他们占用8个字节
--摘录自《极客时间》--《深入理解java虚拟机》
看个例子:内存地址是连续的数组(利用空间局部性),能一次被L1缓存加载完成。
长度为16的row和column数组,在Cache Line 64字节数据块上内存地址是连续的,能被一次加载到Cache Line中,所以在访问数组时,Cache Line命中率高,性能发挥到极致。
public int run(int[] row, int[] column) {
int sum = 0;
for(int i = 0; i < 16; i++ ) {
sum += row[i] * column[i];
}
return sum;
}
而上面例子中变量i则体现了时间局部性,i作为计数器被频繁操作,一直存放在寄存器中,每次从寄存器访问,而不是从主存甚至磁盘访问。虽然连续紧凑的内存分配带来高性能,但并不代表它一直都能带来高性能。如果把它放在多线程中将会发生什么呢?如图:
数据X、Y、Z被加载到同一Cache Line中,线程A在Core1修改X,线程B在Core2上修改Y。根据MESI大法,假设是Core1是第一个发起操作的CPU核,Core1上的L1 Cache Line由S(共享)状态变成M(修改,脏数据)状态,然后告知其他的CPU核,图例则是Core2,引用同一地址的Cache Line已经无效了;当Core2发起写操作时,首先导致Core1将X写回主存,Cache Line状态由M变为I(无效),而后才是Core2从主存重新读取该地址内容,Cache Line状态由I变成E(独占),最后进行修改Y操作, Cache Line从E变成M。可见多个线程操作在同一Cache Line上的不同数据,相互竞争同一Cache Line,导致线程彼此牵制影响,变成了串行程序,降低了并发性。此时我们则需要将共享在多线程间的数据进行隔离,使他们不在同一个Cache Line上,从而提升多线程的性能。
故,共享数据,本来是为了提高程序性能,因为缓存行的原因,导致线程彼此牵制影响,变成了串行程序,降低了并发性,这就是伪共享(本人拙见)
Cache Line伪共享处理方案
处理伪共享的两种方式:
- 增大数组元素的间隔使得不同线程存取的元素位于不同的cache line上。典型的空间换时间。(Linux cache机制与之相关)
- 在每个线程中创建全局数组各个元素的本地拷贝,然后结束后再写回全局数组。
public class Data{
long modifyTime; //修改时间
boolean flag; //标记
long createTime; //创建时间
char key; //唯一标识
int value; //值
}
上面这个对象中,很明显
- 当value变量改变时,modifyTime肯定会改变
- createTime变量和key变量在创建后,就不会再变化。
- flag也经常会变化,不过与modifyTime和value变量毫无关联。
当上面的对象需要由多个线程同时的访问时,从Cache角度来说,就会有一些有趣的问题。当我们没有加任何措施时,Data对象所有的变量极有可能被加载在L1缓存的一行Cache Line中。在高并发访问下,会出现这种问题:
CORE1 | CORE2 | CORE3 | CORE4 |
value | value | value | value |
modifyTime | modifyTime | modifyTime | modifyTime |
flag | flag | flag | flag |
createTime | createTime | createTime | createTime |
key | key | key | key |
高并发下,不做任何措施,全部存放 || 只是变更了一个value和modifyTime
在一个CacheLine中 ||
V
CORE1 | CORE2 | CORE3 | CORE4 |
value | INVALID(失效) | ||
modifyTime | |||
flag | |||
createTime | |||
key |
哦豁,之后获取的所有属性,都需要从内存中重新拉取数据了,性能大大的降低了
看到这里,应该就能够想起,为什么我要在片头举得那个例子了吧,对,他就是决绝伪共享的方案1
Padding 方式
public class DataPadding{
long a1,a2,a3,a4,a5,a6,a7,a8;//防止与前一个对象产生伪共享
int value;
long modifyTime;
long b1,b2,b3,b4,b5,b6,b7,b8;//防止不相关变量伪共享;
boolean flag;
long c1,c2,c3,c4,c5,c6,c7,c8;//
long createTime;
char key;
long d1,d2,d3,d4,d5,d6,d7,d8;//防止与下一个对象产生伪共享
}
在装载对象的时候,在需要进行数据隔离的数据之前,强行用一组无效的对象进行分割,这样的话,每组数据就不会处于同一cacheLine中了
Contended注解方式
在JDK1.8中,新增了一种注解@sun.misc.Contended,来使各个变量在Cache line中分隔开。注意,jvm需要添加参数-XX:-RestrictContended才能开启此功能
用时,可以在类前或属性前加上此注释:
// 类前加上代表整个类的每个变量都会在单独的cache line中
@sun.misc.Contended
@SuppressWarnings("restriction")
public class ContendedData {
int value;
long modifyTime;
boolean flag;
long createTime;
char key;
}
或者这种:
// 属性前加上时需要加上组标签
@SuppressWarnings("restriction")
public class ContendedGroupData {
@sun.misc.Contended("group1")
int value;
@sun.misc.Contended("group1")
long modifyTime;
@sun.misc.Contended("group2")
boolean flag;
@sun.misc.Contended("group3")
long createTime;
@sun.misc.Contended("group3")
char key;
}
就,应该无需多解释了吧
CORE1 | CORE2 | CORE3 | CORE4 |
value | value | value | value |
modifyTime | modifyTime | modifyTime | modifyTime |
flag | flag | flag | flag |
createTime | createTime | createTime | createTime |
key | key | key | key |
高并发下,padding,@Contended两种 ||
方式将会分为三个CacheLine存放 ||
V
CORE1 | CORE2 | CORE3 | CORE4 |
value | INVALID(失效) | ||
modifyTime | |||
flag | flag | flag | flag |
createTime | createTime | createTime | createTime |
key | key | key | key |
over!