偽共享是什么
CPU Cache
眾所周知CPU處理速度與硬盤(pán)、內(nèi)存的訪問(wèn)速度相差過(guò)大,需要通過(guò)CPU緩存進(jìn)行磨合,否則會(huì)導(dǎo)致CPU整體吞吐量受到極大的影響。
而單一層緩存無(wú)論是價(jià)格、命中率、查找速度方面都是不能夠滿足要求的,因此現(xiàn)在很多CPU出現(xiàn)了三級(jí)緩存結(jié)構(gòu),訪問(wèn)速度如下:

其中L1是L2的子集,L2是L3的子集,L1到L3緩存容量依次增大,查找耗時(shí)依次增大,CPU查找順序依次是L1、L2、L3、主存。
L1與CPU core對(duì)應(yīng),是單核獨(dú)占的,不會(huì)出現(xiàn)其他核修改的問(wèn)題。一般L2也是單核獨(dú)占。而L3一般是多核共享,可能操作同一份數(shù)據(jù),那么就有可能出問(wèn)題。
Cache Line
現(xiàn)代CPU讀取數(shù)據(jù)通常以一塊連續(xù)的塊為單位,即緩存行(Cache Line)。所以通常情況下訪問(wèn)連續(xù)存儲(chǔ)的數(shù)據(jù)會(huì)比隨機(jī)訪問(wèn)要快,訪問(wèn)數(shù)組結(jié)構(gòu)通常比鏈結(jié)構(gòu)快,因?yàn)橥ǔ?shù)組在內(nèi)存中是連續(xù)分配的。
PS. JVM標(biāo)準(zhǔn)并未規(guī)定“數(shù)組必須分配在連續(xù)空間”,一些JVM實(shí)現(xiàn)中大數(shù)組不是分配在連續(xù)空間的。
緩存行的大小通常是64字節(jié),這意味著即使只操作1字節(jié)的數(shù)據(jù),CPU最少也會(huì)讀取這個(gè)數(shù)據(jù)所在的連續(xù)64字節(jié)數(shù)據(jù)。
緩存失效
根據(jù)主流CPU為保證緩存有效性的MESI協(xié)議的簡(jiǎn)單理解,如果一個(gè)核正在使用的數(shù)據(jù)所在的緩存行被其他核修改,那么這個(gè)緩存行會(huì)失效,需要重新讀取緩存。
False Sharing
如果多個(gè)核的線程在操作同一個(gè)緩存行中的不同變量數(shù)據(jù),那么就會(huì)出現(xiàn)頻繁的緩存失效,即使在代碼層面看這兩個(gè)線程操作的數(shù)據(jù)之間完全沒(méi)有關(guān)系。
這種不合理的資源競(jìng)爭(zhēng)情況學(xué)名偽共享(False Sharing),會(huì)嚴(yán)重影響機(jī)器的并發(fā)執(zhí)行效率。
偽共享示例
// 多個(gè)線程,每個(gè)線程操作一個(gè)VolatileLong數(shù)組中的元素
// VolatileLong是否進(jìn)行填充會(huì)影響最終結(jié)果
// 為填充時(shí)會(huì)產(chǎn)生偽共享問(wèn)題,運(yùn)行更慢,填充后不會(huì)
public class FalseShareTest implements Runnable {
public static int NUM_THREADS = 4;
public final static long ITERATIONS = 50L * 1000L * 1000L;
private final int arrayIndex;
private static VolatileLong[] longs;
public static long SUM_TIME = 0l;
public FalseShareTest(final int arrayIndex) {
this.arrayIndex = arrayIndex;
}
public static void main(final String[] args) throws Exception {
Thread.sleep(10000);
// 多個(gè)線程操作多個(gè)VolatileLong
for(int j=0; j<10; j++){
// 初始化
System.out.println(j);
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();
// 構(gòu)造并啟動(dòng)線程
runTest();
final long end = System.nanoTime();
SUM_TIME += end - start;
}
System.out.println("平均耗時(shí):"+SUM_TIME/10);
}
private static void runTest() throws InterruptedException {
// 創(chuàng)建每個(gè)線程, 每個(gè)線程操作一個(gè)VolatileLong
Thread[] threads = new Thread[NUM_THREADS];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(new FalseShareTest(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; // 注釋此行,結(jié)果區(qū)別很大
}
}
VolatileLong是否使用6個(gè)long變量填充,結(jié)果相差很多。
使用填充,會(huì)避免偽共享,速度更快。
偽共享如何避免
Java8以下的版本
在Java8以下的版本中,可以使用填充的方式進(jìn)行避免,比如百度的snowflake實(shí)現(xiàn)中使用的PaddedAtomicLong:
/**
* Represents a padded {@link AtomicLong} to prevent the FalseSharing problem<p>
*
* The CPU cache line commonly be 64 bytes, here is a sample of cache line after padding:<br>
* 64 bytes = 8 bytes (object reference) + 6 * 8 bytes (padded long) + 8 bytes (a long value)
*
* @author yutianbao
*/
public class PaddedAtomicLong extends AtomicLong {
private static final long serialVersionUID = -3415778863941386253L;
/** Padded 6 long (48 bytes) */
public volatile long p1, p2, p3, p4, p5, p6 = 7L;
/**
* Constructors from {@link AtomicLong}
*/
public PaddedAtomicLong() {
super();
}
public PaddedAtomicLong(long initialValue) {
super(initialValue);
}
/**
* To prevent GC optimizations for cleaning unused padded references
*/
public long sumPaddingToPreventOptimization() {
return p1 + p2 + p3 + p4 + p5 + p6;
}
}
對(duì)象引用8字節(jié),使用了6個(gè)long變量48字節(jié)進(jìn)行填充,以及一個(gè)long型的值,一共64字節(jié)。
使用了sumPaddingToPreventOptimization方法規(guī)避編譯器或GC優(yōu)化沒(méi)使用的變量。
Java8及以上的版本
從Java8開(kāi)始原生支持避免偽共享,可以使用@Contended注解:
public class Point {
int x;
@Contended
int y;
}
詳見(jiàn)@Contended注解使用方法。
@Contended 注解會(huì)增加目標(biāo)實(shí)例大小,要謹(jǐn)慎使用。默認(rèn)情況下,除了 JDK 內(nèi)部的類(lèi),JVM 會(huì)忽略該注解。要應(yīng)用代碼支持的話,要設(shè)置 -XX:-RestrictContended=false,它默認(rèn)為 true(意味僅限 JDK 內(nèi)部的類(lèi)使用)。當(dāng)然,也有個(gè) –XX: EnableContented 的配置參數(shù),來(lái)控制開(kāi)啟和關(guān)閉該注解的功能,默認(rèn)是 true,如果改為 false,可以減少 Thread 和 ConcurrentHashMap 類(lèi)的大小。參加《Java性能權(quán)威指南》210 頁(yè)。
參考資料
偽共享(false sharing),并發(fā)編程無(wú)聲的性能殺手 - 博客園
偽共享(False Sharing)和緩存行(Cache Line) 大雜燴 - 簡(jiǎn)書(shū)
本文搬自我的博客,歡迎參觀!