偽共享
什么是偽共享
? 為了解決計算機系統中主存與CPU之間的運行速度差問題,會在CPU與主存之間添加一級或者多級高速緩沖存盤器(Cache),這個Cache一般集中于CPU內部當中,所以也叫CPU Cache,圖中是兩級Cache結構

? 在cache中,其中的每一行稱為一個cache行,cache行是cache與主存進行資料交換的單位,cache行一般為2的冪次數位元組,

? 當CPU訪問某一個變數的時候,首先會查看CPU cache中是否有該變數,如果有,則直接從快取中拿,否則就去快取中獲取該變數,然后把該變數所在的記憶體區域的一個大cache行大小復制到caceh中,由于存放到cache行的是記憶體塊而不是單個變數,所以可能會把多個變數放到一個cache行中,當多個執行緒同時修改一個快取行里面的多個變數時同時只能有一個執行緒操作快取行,所以相比每個變數放到一個快取行,性能過會有所下降,這就是偽共享,如圖: !
? 在該圖中,變數X和變數Y同時被放到CPU的一級快取和二級快取,當執行緒1使用CPU1對變數X進行更新的時候,首先會修改CPU1的一級快取X所在的快取行,這時候在快取一致性的協議下,CPU2中變數X對應的快取行失效,那么執行緒2在寫入變數X的時候就只能去二級快取里找,這就破壞了一級快取,而一級快取更新比二級塊,這也說明了多個執行緒不能同時去修改自己所使用的CPU中相同快取行里面的數量,更壞的情況是,如果CPU中只有一級快取,則會頻繁的訪問主存,
為何會出現偽共享
? 偽共享的產生是因為多個變數同時的被放入到一個快取行中,并且過個執行緒同時去寫入記憶體快取行中不同的變數,那么為什么多個變數會被放到一個快取行中呢?其實是因為快取與記憶體交換的單位就是快取行,當CPU要訪問的變數沒有在快取中找到的時候,根據程式運行的區域性原理,會把該變數所在記憶體中大小為快取行的記憶體存入快取行,
? 當單執行緒下多個變數被放到同一個快取行對性能影響嗎?其實在正常情況下單執行緒訪問時將陣列元素放到一個或者多個快取行對代碼執行是有利的,因為資料都在快取中,代碼執行會更快,比較以下代碼:
public class TestForContent {
static final int LINE_NUM=1024;
static final int COLUMN_NUM=1024;
public static void main(String[] args) {
long[][] array=new long[LINE_NUM][COLUMN_NUM];
long startTime=System.currentTimeMillis();
for (int i = 0; i < LINE_NUM; i++) {
for (int j = 0; j < COLUMN_NUM; j++) {
array[i][j]=i*2+j;
}
}
long endTime=System.currentTimeMillis();
System.out.println("cache time:"+(endTime-startTime));
}
}
執行結果
cache time:9
Process finished with exit code 0
public class TestForContent {
static final int LINE_NUM=1024;
static final int COLUMN_NUM=1024;
public static void main(String[] args) {
long[][] array=new long[LINE_NUM][COLUMN_NUM];
long startTime=System.currentTimeMillis();
for (int i = 0; i < COLUMN_NUM; i++) {
for (int j = 0; j < LINE_NUM; j++) {
array[j][i]=i*2+j;
}
}
long endTime=System.currentTimeMillis();
System.out.println("not cache time:"+(endTime-startTime));
}
}
執行結果
not cache time:14
Process finished with exit code 0
? 經過多次測驗,有快取所需的時間要少于沒有快取執行的時間,這是因為陣列內元素地址是連續的,當訪問陣列第一個元素的時候,就會把第一個元素后的若干元素一塊放入快取行內,這樣順序訪問數字元素就會在快取中直接命中,因為就交少了去主存中取資料的時間,后續訪問也是這樣,一次記憶體訪問可以讓后面訪問直接在快取命中,
? 而沒有快取代碼則是跳躍式訪問陣列元素,不是順序的,這破壞了程式訪問的區域性原則,并且快取是有限容量的,當快取滿后會根據一定的淘汰演算法替換快取行,這會導致從內置過來的快取行還沒等到被讀取就被替換掉了,
? 所以在單執行緒下順序修改一個快取行中的多個變數,會充分利用程式運行的區域性原理,從而加快程式的運行,而在多執行緒下修改一個快取中的多個變數時會競爭快取行,從而降低運行性能,
如何避免偽共享
? 在JDK8以前一般都是通過位元組填充 方式來避免該問題,也就是創建變數的時候使用填充欄位填充該變數所在的快取行,這樣就避免了將多個變數放到同一個緩沖行中,例如如下代碼:
public final static class FilledLong{
public volatile long value=https://www.cnblogs.com/xiaomitu/archive/2021/10/06/0L;
public long p1,p2,p3,p4,p5,p6;
}
? 假如快取行為64位元組,那么我們在FilledLong類里面填充了6個long型別的變數,每個long型別占用8位元組,加上value變數總共56位元組,另外,這里FilledLong是一個類物件,而類物件的位元組碼的物件頭占用8個位元組,因此一個FilledLong物件就會占用64個位元組,就剛好是放入到一個快取行中,
JDK8提供了一個sum.mis.Contended注解,用來解決偽共享問題,將上面的代碼修改如下,
@sum.mis.Contended
public final static class FilledLong{
public volatile long value=https://www.cnblogs.com/xiaomitu/archive/2021/10/06/0L;
}
在這里注解可以修飾類,當然注解也可用于修飾變數,例如:
@sum.mis.Contented("tlr")
long threadLongLoalRandomeSeed;
? 在默認情況下,@Contended注解只用于Java核心類,比如rt下包的類,如果用戶下的類需要使用這個注解,則需要添加JVM引數:-XX:-RestrictContented,填充的位元組默認寬度為128位元組,如果需要手動修改則可以設定:-XX:CntendedPaddingWidth引數,
小結
? 該部分主要講了偽共享是怎么產生的,以及如何避免,在單執行緒下訪問一個快取行里面的多個變數反而會對程式運行起加速作用,但是多執行緒同時訪問同一個快取行里面的多個變數才會出現偽共享,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/305848.html
標籤:其他
上一篇:Java秒殺系統三:web層
