博客鏈接:http://www.ideabuffer.cn/2017/05/12/Java8使用-sun-misc-Contended避免偽共享/
什么是偽共享
緩存系統(tǒng)中是以緩存行(cache line)為單位存儲的乞封。緩存行是2的整數(shù)冪個連續(xù)字節(jié)惧浴,一般為32-256個字節(jié)。最常見的緩存行大小是64個字節(jié)脂矫。當多線程修改互相獨立的變量時恩沛,如果這些變量共享同一個緩存行在扰,就會無意中影響彼此的性能,這就是偽共享雷客。
緩存行上的寫競爭是運行在SMP系統(tǒng)中并行線程實現(xiàn)可伸縮性最重要的限制因素芒珠。有人將偽共享描述成無聲的性能殺手,因為從代碼中很難看清楚是否會出現(xiàn)偽共享搅裙。
為了讓可伸縮性與線程數(shù)呈線性關系皱卓,就必須確保不會有兩個線程往同一個變量或緩存行中寫。兩個線程寫同一個變量可以在代碼中發(fā)現(xiàn)部逮。為了確定互相獨立的變量是否共享了同一個緩存行娜汁,就需要了解緩存行和對象的內(nèi)存布局,有關緩存行和對象內(nèi)存布局可以參考我的另外兩篇文章理解CPU Cache和Java對象內(nèi)存布局兄朋。
下面的圖說明了偽共享的問題:
假設在核心1上運行的線程想更新變量X掐禁,同時核心2上的線程想要更新變量Y。不幸的是,這兩個變量在同一個緩存行中傅事。每個線程都要去競爭緩存行的所有權來更新變量缕允。如果核心1獲得了所有權,緩存子系統(tǒng)將會使核心2中對應的緩存行失效蹭越。當核心2獲得了所有權然后執(zhí)行更新操作障本,核心1就要使自己對應的緩存行失效。這會來來回回的經(jīng)過L3緩存响鹃,大大影響了性能驾霜。如果互相競爭的核心位于不同的插槽,就要額外橫跨插槽連接买置,問題可能更加嚴重寄悯。
避免偽共享
假設有一個類中,只有一個long類型的變量:
public final static class VolatileLong {
public volatile long value = 0L;
}
這時定義一個VolatileLong類型的數(shù)組堕义,然后讓多個線程同時并發(fā)訪問這個數(shù)組,這時可以想到脆栋,在多個線程同時處理數(shù)據(jù)時倦卖,數(shù)組中的多個VolatileLong對象可能存在同一個緩存行中,通過上文可知椿争,這種情況就是偽共享怕膛。
怎么樣避免呢?在Java 7之前秦踪,可以在屬性的前后進行padding褐捻,例如:
public final static class VolatileLong {
volatile long p0, p1, p2, p3, p4, p5, p6;
public volatile long value = 0;
volatile long q0, q1, q2, q3, q4, q5, q6;
}
通過Java對象內(nèi)存布局文章中結(jié)尾對paddign的分析可知,由于都是long類型的變量椅邓,這里就是按照聲明的順序分配內(nèi)存柠逞,那么這可以保證在同一個緩存行中只有一個VolatileLong對象。
__ 這里有一個問題:據(jù)說Java7優(yōu)化了無用字段景馁,會使這種形式的補位無效板壮,但經(jīng)過測試,無論是在JDK 1.7 還是 JDK 1.8中合住,這種形式都是有效的绰精。網(wǎng)上有關偽共享的文章基本都是來自Martin的兩篇博客,這種優(yōu)化方式也是在他的博客中提到的透葛。但國內(nèi)的文章貌似根本就沒有驗證過而直接引用了此觀點笨使,這也確實迷惑了一大批同學!__
在Java 8中僚害,提供了@sun.misc.Contended注解來避免偽共享硫椰,原理是在使用此注解的對象或字段的前后各增加128字節(jié)大小的padding,使用2倍于大多數(shù)硬件緩存行的大小來避免相鄰扇區(qū)預取導致的偽共享沖突。具體可以參考http://mail.openjdk.java.net/pipermail/hotspot-dev/2012-November/007309.html最爬。
下面用代碼來看一下加padding和不加的效果:
運行環(huán)境:JDK 1.8涉馁,macOS 10.12.4,2.2 GHz Intel Core i7爱致,四核-八線程
public 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];
// private static VolatileLong2[] longs = new VolatileLong2[NUM_THREADS];
// private static VolatileLong3[] longs = new VolatileLong3[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 {
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;
}
// long padding避免false sharing
// 按理說jdk7以后long padding應該被優(yōu)化掉了烤送,但是從測試結(jié)果看padding仍然起作用
public final static class VolatileLong2 {
volatile long p0, p1, p2, p3, p4, p5, p6;
public volatile long value = 0L;
volatile long q0, q1, q2, q3, q4, q5, q6;
}
/**
* jdk8新特性,Contended注解避免false sharing
* Restricted on user classpath
* Unlock: -XX:-RestrictContended
*/
@sun.misc.Contended
public final static class VolatileLong3 {
public volatile long value = 0L;
}
}
VolatileLong對象只有一個long類型的字段糠悯,VolatileLong2加了padding帮坚,下面分別執(zhí)行看下時間:
duration = 57293259577
duration = 4679059000
沒加padding時用了大概57秒,加padding后用時大概4.6秒互艾,可見加padding后有效果了试和。
在Java8中提供了@sun.misc.Contended來避免偽共享,例如這里的VolatileLong3纫普,在運行時需要設置JVM啟動參數(shù)-XX:-RestrictContended
阅悍,運行一下結(jié)果如下:
duration = 4756952426
結(jié)果與加padding的時間差不多。
下面看一下VolatileLong對象在運行時的內(nèi)存大凶蚣凇(參考Java對象內(nèi)存布局):
再來看下VolatileLong2對象在運行時的內(nèi)存大薪谑印:
因為多了14個long類型的變量,所以24+8*14=136字節(jié)假栓。
下面再來看下使用@sun.misc.Contended注解后的對象內(nèi)存大醒靶小:
在堆內(nèi)存中并沒有看到對變量進行padding,大小與VolatileLong對象是一樣的匾荆。
這就奇怪了拌蜘,看起來與VolatileLong沒什么不一樣,但看一下內(nèi)存的地址牙丽,用十六進制算一下简卧,兩個VolatileLong對象地址相差24字節(jié),而兩個VolatileLong3對象地址相差280字節(jié)烤芦。這就是前面提到的@sun.misc.Contended注解會在對象或字段的前后各增加128字節(jié)大小的padding贞滨,那么padding的大小就是256字節(jié),再加上對象的大小24字節(jié)拍棕,結(jié)果就是280字節(jié)晓铆,所以確實是增加padding了。
八線程運行比四線程運行還快绰播?
根據(jù)上面的代碼骄噪,把NUM_THREADS改為8,測試看下結(jié)果:
VolatileLong: 44305002641
VolatileLong2: 7100172492
VolatileLong3: 7335024041
可以看到蠢箩,加了padding和@sun.misc.Contended注解的運行時間多了不到1倍链蕊,而VolatileLong運行的時間比線程數(shù)是4的時候還要短事甜,這是為什么呢?
再說一下滔韵,我的CPU是四核八線程逻谦,每個核有一個L1 Cache,那么我的環(huán)境一共有4個L1 Cache陪蜻,所以邦马,2個CPU線程會共享同一個L1 Cache;由于VolatileLong對象占用24字節(jié)內(nèi)存宴卖,而代碼中VolatileLong對象是保存在數(shù)組中的滋将,所以內(nèi)存是連續(xù)的,2個VolatileLong對象的大小是48字節(jié)症昏,這樣一來随闽,對于緩存行大小是64字節(jié)來說,每個緩存行只能存放2個VolatileLong對象肝谭。
通過上面的分析可知掘宪,偽共享發(fā)生在L3 Cache,如果每個核操作的數(shù)據(jù)不在同一個緩存行中攘烛,那么就會避免偽共享的發(fā)生添诉,所以,8個線程的情況下其實是CPU線程共享了L1 Cache医寿,所以執(zhí)行的時間可能比4線程的情況還要短。下面看下執(zhí)行時4線程和8線程的CPU使用情況:
可以看到蘑斧,在4線程時靖秩,線程被平均分配到了4個核中,這樣一來竖瘾,L1 Cache肯定是不能共享的沟突,這時會發(fā)生偽共享;而8線程時捕传,每個核都使用了2個線程惠拭,這時L1 Cache是可以共享的,這在一定程度上能減少偽共享的發(fā)生庸论,從而時間會變短(也不一定职辅,但總體來說8線程的情況與4線程的運行時間幾乎不會向加padding和注解的方式差那么多)。
在Windows上情況就不太一樣了聂示,在雙核四線程的CPU上域携,測試結(jié)果并不和mac中一樣,在不加padding和注解時鱼喉,2線程和4線程執(zhí)行的時間都是將近差了1倍秀鞭,看下使用2個線程在Windows中執(zhí)行的時候CPU的使用情況:
雖然只使用了2個線程趋观,但從圖像上來看,似乎都在工作锋边,即使把線程數(shù)量設置為1也是這種情況皱坛。這應該是Windows和UNIX對CPU線程調(diào)度的方式不一樣,具體我現(xiàn)在也不太清楚他們之間的差別豆巨,希望有知道的同學告知剩辟,感謝。
@sun.misc.Contended注解
上文中將@sun.misc.Contended注解用在了對象上搀矫,@sun.misc.Contended注解還可以指定某個字段抹沪,并且可以為字段進行分組,下面通過代碼來看下:
/**
* VM Options:
* -javaagent:/Users/sangjian/dev/source-files/classmexer-0_03/classmexer.jar
* -XX:-RestrictContended
*/
public class ContendedTest {
byte a;
@sun.misc.Contended("a")
long b;
@sun.misc.Contended("a")
long c;
int d;
private static Unsafe UNSAFE;
static {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
UNSAFE = (Unsafe) f.get(null);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws NoSuchFieldException {
System.out.println("offset-a: " + UNSAFE.objectFieldOffset(ContendedTest.class.getDeclaredField("a")));
System.out.println("offset-b: " + UNSAFE.objectFieldOffset(ContendedTest.class.getDeclaredField("b")));
System.out.println("offset-c: " + UNSAFE.objectFieldOffset(ContendedTest.class.getDeclaredField("c")));
System.out.println("offset-d: " + UNSAFE.objectFieldOffset(ContendedTest.class.getDeclaredField("d")));
ContendedTest contendedTest = new ContendedTest();
// 打印對象的shallow size
System.out.println("Shallow Size: " + MemoryUtil.memoryUsageOf(contendedTest) + " bytes");
// 打印對象的 retained size
System.out.println("Retained Size: " + MemoryUtil.deepMemoryUsageOf(contendedTest) + " bytes");
}
}
這里還是使用到了classmexer.jar瓤球,可以參考Java對象內(nèi)存布局中的說明融欧。
這里在變量b和c中使用了@sun.misc.Contended注解,并將這兩個變量分為1組卦羡,執(zhí)行結(jié)果如下:
offset-a: 16
offset-b: 152
offset-c: 160
offset-d: 12
Shallow Size: 296 bytes
Retained Size: 296 bytes
可見int類型的變量的偏移地址是12噪馏,也就是在對象頭后面,因為它正好是4個字節(jié)绿饵,然后是變量a欠肾。@sun.misc.Contended注解的變量會加到對象的最后面,這里就是b和c了拟赊,那么b的偏移地址是152刺桃,之前說過@sun.misc.Contended注解會在變量前后各加128字節(jié),而byte類型的變量a分配完內(nèi)存后這時起始地址應該是從17開始吸祟,因為byte類型占1字節(jié)瑟慈,那么應該補齊到24,所以b的起始地址是24+128=152屋匕,而c的前面并不用加128字節(jié)葛碧,因為b和c被分為了同一組。
我們算一下c分配完內(nèi)存后过吻,這時的地址應該到了168进泼,然后再加128字節(jié),最后大小就是296纤虽。內(nèi)存結(jié)構(gòu)如下:
| d:12~16 | --- | a:16~17 | --- | 17~24 | --- | 24~152 | --- | b:152~160 | --- | c:160~168 | --- | 168~296 |
現(xiàn)在把b和c分配到不同的組中乳绕,代碼做如下修改:
/**
* VM Options:
* -javaagent:/Users/sangjian/dev/source-files/classmexer-0_03/classmexer.jar
* -XX:-RestrictContended
*/
public class ContendedTest {
byte a;
@sun.misc.Contended("a")
long b;
@sun.misc.Contended("b")
long c;
int d;
private static Unsafe UNSAFE;
static {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
UNSAFE = (Unsafe) f.get(null);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws NoSuchFieldException {
System.out.println("offset-a: " + UNSAFE.objectFieldOffset(ContendedTest.class.getDeclaredField("a")));
System.out.println("offset-b: " + UNSAFE.objectFieldOffset(ContendedTest.class.getDeclaredField("b")));
System.out.println("offset-c: " + UNSAFE.objectFieldOffset(ContendedTest.class.getDeclaredField("c")));
System.out.println("offset-d: " + UNSAFE.objectFieldOffset(ContendedTest.class.getDeclaredField("d")));
ContendedTest contendedTest = new ContendedTest();
// 打印對象的shallow size
System.out.println("Shallow Size: " + MemoryUtil.memoryUsageOf(contendedTest) + " bytes");
// 打印對象的 retained size
System.out.println("Retained Size: " + MemoryUtil.deepMemoryUsageOf(contendedTest) + " bytes");
}
}
運行結(jié)果如下:
offset-a: 16
offset-b: 152
offset-c: 288
offset-d: 12
Shallow Size: 424 bytes
Retained Size: 424 bytes
可以看到,這時b和c中增加了128字節(jié)的padding逼纸,結(jié)構(gòu)也就變成了:
| d:12~16 | --- | a:16~17 | --- | 17~24 | --- | 24~152 | --- | b:152~160 | --- | 160~288 | --- | c:288~296 | --- | 296~424 |