一.偽共享與緩存行
1.CPU緩存架構(gòu)
CPU 是計算機的心臟,所有運算和程序最終都要由它來執(zhí)行。
主內(nèi)存(RAM)是數(shù)據(jù)存放的地方,CPU 和主內(nèi)存之間有好幾級緩存,因為即使直接訪問主內(nèi)存也是非常慢的。
CPU的速度要遠遠大于內(nèi)存的速度,為了解決這個問題,CPU引入了三級緩存:L1,L2和L3三個級別,L1最靠近CPU,L2次之,L3離CPU最遠,L3之后才是主存。速度是L1>L2>L3>主存。越靠近CPU的容量越小。CPU獲取數(shù)據(jù)會依次從三級緩存中查找
當(dāng)CPU要讀取一個數(shù)據(jù)時,首先從一級緩存中查找,如果沒有找到再從二級緩存中查找,如果還是沒有就從三級緩存或內(nèi)存中查找。一般來說,每級緩存的命中率大概都在80%左右,也就是說全部數(shù)據(jù)量的80%都可以在一級緩存中找到,只剩下20%的總數(shù)據(jù)量才需要從二級緩存、三級緩存或內(nèi)存中讀取,由此可見一級緩存是整個CPU緩存架構(gòu)中最為重要的部分。
?2.什么是偽共享
計算機系統(tǒng)中為了解決主內(nèi)存與CPU運行速度的差距,在CPU與主內(nèi)存之間添加了一級或者多級高速緩沖存儲器(Cache),這個Cache一般是集成到CPU內(nèi)部的,所以也叫 CPU Cache,如下圖是兩級cache結(jié)構(gòu)
Cache內(nèi)部是按行存儲的,其中每一行稱為一個緩存行,緩存行是Cache與主內(nèi)存進行數(shù)據(jù)交換的單位,緩存行的大小一般為2的冪次數(shù)字節(jié)。?
當(dāng)CPU訪問某一個變量時候,首先會去看CPU Cache內(nèi)是否有該變量,如果有則直接從中獲取,否者就去主內(nèi)存里面獲取該變量,然后把該變量所在內(nèi)存區(qū)域的一個Cache行大小的內(nèi)存拷貝到Cache(cache行是Cache與主內(nèi)存進行數(shù)據(jù)交換的單位)。由于存放到Cache行的的是內(nèi)存塊而不是單個變量,所以可能會把多個變量存放到了一個cache行。當(dāng)多個線程同時修改一個緩存行里面的多個變量時候,由于同時只能有一個線程操作緩存行,所以相比每個變量放到一個緩存行性能會有所下降,這就是偽共享。
?文章來源地址http://www.zghlxwxcb.cn/news/detail-518334.html
?如上圖變量x,y同時被放到了CPU的一級和二級緩存,當(dāng)線程1使用CPU1對變量x進行更新時候,首先會修改cpu1的一級緩存變量x所在緩存行,這時候緩存一致性協(xié)議會導(dǎo)致cpu2中變量x對應(yīng)的緩存行失效,那么線程2寫入變量x的時候就只能去二級緩存去查找,這就破壞了一級緩存,而一級緩存比二級緩存更快。更壞的情況下如果cpu只有一級緩存,那么會導(dǎo)致頻繁的直接訪問主內(nèi)存。
3.為何會出現(xiàn)偽共享
? 偽共享的產(chǎn)生是因為多個變量被放入了一個緩存行,并且多個線程同時去寫入緩存行中不同變量。那么為何多個變量會被放入一個緩存行那。其實是因為Cache與內(nèi)存交換數(shù)據(jù)的單位就是Cache,當(dāng)CPU要訪問的變量沒有在Cache命中時候,根據(jù)程序運行的局部性原理會把該變量在內(nèi)存中大小為Cache行的內(nèi)存放如緩存行。
4.Java中的偽共享
?解決偽共享最直接的方法就是填充(padding),例如下面的VolatileLong,一個long占8個字節(jié),Java的對象頭占用8個字節(jié)(32位系統(tǒng))或者12字節(jié)(64位系統(tǒng),默認(rèn)開啟對象頭壓縮,不開啟占16字節(jié))。一個緩存行64字節(jié),那么我們可以填充6個long(6 * 8 = 48 個字節(jié))。
? 現(xiàn)在,我們學(xué)習(xí)JVM對象的內(nèi)存模型。所有的Java對象都有8字節(jié)的對象頭,前四個字節(jié)用來保存對象的哈希碼和鎖的狀態(tài),前3個字節(jié)用來存儲哈希碼,最后一個字節(jié)用來存儲鎖狀態(tài),一旦對象上鎖,這4個字節(jié)都會被拿出對象外,并用指針進行鏈接。剩下4個字節(jié)用來存儲對象所屬類的引用。對于數(shù)組來講,還有一個保存數(shù)組大小的變量,為4字節(jié)。每一個對象的大小都會對齊到8字節(jié)的倍數(shù),不夠8字節(jié)部分需要填充。為了保證效率,Java編譯器在編譯Java對象的時候,通過字段類型對Java對象的字段進行排序,如下表所示。
?? 因此,我們可以在任何字段之間通過填充長整型的變量把熱點變量隔離在不同的緩存行中,通過減少偽同步,在多核心CPU中能夠極大的提高效率。
最簡單的方式
/**
* 緩存行填充父類
*/
public class DataPadding {
//填充 6個long類型字段 8*4 = 48 個字節(jié)
private long p1, p2, p3, p4, p5, p6;
//需要操作的數(shù)據(jù)
private long data;
}
因為JDK1.7以后就自動優(yōu)化代碼會刪除無用的代碼,在JDK1.7以后的版本這些不生效了
繼承的方式
/**
* 緩存行填充父類
*/
public class DataPadding {
//填充 6個long類型字段 8*4 = 48 個字節(jié)
private long p1, p2, p3, p4, p5, p6;
}
繼承緩存填充類
/**
* 繼承DataPadding
*/
public class VolatileData extends DataPadding {
// 占用 8個字節(jié) +48 + 對象頭 = 64字節(jié)
private long data = 0;
public VolatileData() {
}
public VolatileData(long defValue) {
this.data = defValue;
}
public long accumulationAdd() {
//因為單線程操作不需要加鎖
data++;
return data;
}
public long getValue() {
return data;
}
}
這樣在JDK1.8中是可以使用的
@Contended注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface Contended {
String value() default "";
}
Contended注解可以用于類型上和屬性上,加上這個注解之后虛擬機會自動進行填充,從而避免偽共享。這個注解在Java8 ConcurrentHashMap、ForkJoinPool和Thread等類中都有應(yīng)用。我們來看一下Java8中ConcurrentHashMap中如何運用Contended這個注解來解決偽共享問題。以下說的ConcurrentHashMap都是Java8版本。
注意:在Java8中提供了**@sun.misc.Contended來避免偽共享時,在運行時需要設(shè)置JVM啟動參數(shù)-XX:-RestrictContended**否則可能不生效。
緩存行填充的威力
/**
* 緩存行測試
*/
public class CacheLineTest {
/**
* 是否啟用緩存行填充
*/
private final boolean isDataPadding = false;
/**
* 正常定義的變量
*/
private volatile long x = 0;
private volatile long y = 0;
private volatile long z = 0;
/**
* 通過緩存行填充的變量
*/
private volatile VolatileData volatileDataX = new VolatileData(0);
private volatile VolatileData volatileDataY = new VolatileData(0);
private volatile VolatileData volatileDataZ = new VolatileData(0);
/**
* 循環(huán)次數(shù)
*/
private final long size = 100000000;
/**
* 進行累加操作
*/
public void accumulationX() {
//計算耗時
long currentTime = System.currentTimeMillis();
long value = 0;
//循環(huán)累加
for (int i = 0; i < size; i++) {
//使用緩存行填充的方式
if (isDataPadding) {
value = volatileDataX.accumulationAdd();
} else {
//不使用緩存行填充的方式 因為時單線程操作不需要加鎖
value = (++x);
}
}
//打印
System.out.println(value);
//打印耗時
System.out.println("耗時:" + (System.currentTimeMillis() - currentTime));
}
/**
* 進行累加操作
*/
public void accumulationY() {
long currentTime = System.currentTimeMillis();
long value = 0;
for (int i = 0; i < size; i++) {
if (isDataPadding) {
value = volatileDataY.accumulationAdd();
} else {
value = ++y;
}
}
System.out.println(value);
System.out.println("耗時:" + (System.currentTimeMillis() - currentTime));
}
/**
* 進行累加操作
*/
public void accumulationZ() {
long currentTime = System.currentTimeMillis();
long value = 0;
for (int i = 0; i < size; i++) {
if (isDataPadding) {
value = volatileDataZ.accumulationAdd();
} else {
value = ++z;
}
}
System.out.println(value);
System.out.println("耗時:" + (System.currentTimeMillis() - currentTime));
}
public static void main(String[] args) {
//創(chuàng)建對象
CacheLineTest cacheRowTest = new CacheLineTest();
//創(chuàng)建線程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
//啟動三個線程個調(diào)用他們各自的方法
executorService.execute(() -> cacheRowTest.accumulationX());
executorService.execute(() -> cacheRowTest.accumulationY());
executorService.execute(() -> cacheRowTest.accumulationZ());
executorService.shutdown();
}
}
不使用緩存行填充測試
/**
* 是否啟用緩存行填充
*/
private final boolean isDataPadding = false;
輸出
100000000
耗時:7960
100000000
耗時:7984
100000000
耗時:7989
使用緩存行填充測試
/**
* 是否啟用緩存行填充
*/
private final boolean isDataPadding = true;
輸出
100000000
耗時:176
100000000
耗時:178
100000000
耗時:182
同樣的結(jié)構(gòu)他們之間差了 將近 50倍的速度差距
總結(jié)
? 當(dāng)多個線程同時對共享的緩存行進行寫操作的時候,因為緩存系統(tǒng)自身的緩存一致性原則,會引發(fā)偽共享問題,解決的常用辦法是將共享變量根據(jù)緩存行大小進行補充對齊,使其加載到緩存時能夠獨享緩存行,避免與其他共享變量存儲在同一個緩存行。文章來源:http://www.zghlxwxcb.cn/news/detail-518334.html
?
到了這里,關(guān)于JAVA中的偽共享與緩存行的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!