Java中的偽共享以及應對方案
什麼是偽共享
CPU快取系統中是以快取行(cache line)為單位儲存的。目前主流的CPU Cache的Cache Line大小都是64Bytes。在多執行緒情況下,如果需要修改“共享同一個快取行的變數”,就會無意中影響彼此的效能,這就是偽共享(False Sharing)。
CPU的三級快取
由於CPU的速度遠遠大於記憶體速度,所以CPU設計者們就給CPU加上了快取(CPU Cache)。 以免運算被記憶體速度拖累。(就像我們寫程式碼把共享資料做Cache不想被DB存取速度拖累一樣),CPU Cache分成了三個級別:L1,L2,L3。級別越小越接近CPU, 所以速度也更快, 同時也代表著容量越小。
CPU獲取資料回依次從L1,L2,L3中查詢,如果都找不到則會直接向記憶體查詢。
快取行
由於共享變數在CPU快取中的儲存是以快取行為單位,一個快取行可以儲存多個變數(存滿當前快取行的位元組數);而CPU對快取的修改又是以快取行為最小單位的,那麼就會出現上訴的偽共享問題。
Cache Line可以簡單的理解為CPU Cache中的最小快取單位,今天的CPU不再是按位元組訪問記憶體,而是以64位元組為單位的塊(chunk)拿取,稱為一個快取行(cache line)。當你讀一個特定的記憶體地址,整個快取行將從主存換入快取,並且訪問同一個快取行內的其它值的開銷是很小的。
看如下程式碼示例:
int[] arr = new int[64 * 1024 * 1024];
long start = System.nanoTime();
for (int i = 0; i < arr.length; i++) {
arr[i] *= 3;
}
System.out.println(System.nanoTime() - start);
long start2 = System.nanoTime();
for (int i = 0; i < arr.length; i += 16) {
arr[i] *= 3;
}
System.out.println(System.nanoTime() - start2);
表面上看,第二個迴圈工作量為第一個迴圈的1/16;但是執行時間是相差不大的,假設在記憶體規整的情況下,每16個int 佔用4*16=64位元組,正好一個快取行,也就是說這兩個迴圈訪問記憶體的次數是一致的。導致耗時相差不大。
快取關聯性
目前常用的快取設計是N路組關聯(N-Way Set Associative Cache),他的原理是把一個快取按照N個Cache Line作為一組(Set),快取按組劃為等分。每個記憶體塊能夠被對映到相對應的set中的任意一個快取行中。比如一個16路快取,16個Cache Line作為一個Set,每個記憶體塊能夠被對映到相對應的Set
中的16個CacheLine中的任意一個。一般地,具有一定相同低bit位地址的記憶體塊將共享同一個Set。
下圖為一個2-Way的Cache。由圖中可以看到Main Memory中的Index0,2,4都對映在Way0的不同CacheLine中,Index1,3,5都對映在Way1的不同CacheLine中。
MESI協議
多核CPU都有自己的專有快取(一般為L1,L2),以及同一個CPU插槽之間的核共享的快取(一般為L3)。不同核心的CPU快取中難免會載入同樣的資料,那麼如何保證資料的一致性呢,就是MESI協議了。
在MESI協議中,每個Cache line有4個狀態,可用2個bit表示,它們分別是:
M(Modified):這行資料有效,資料被修改了,和記憶體中的資料不一致,資料只存在於本Cache中;
E(Exclusive):這行資料有效,資料和記憶體中的資料一致,資料只存在於本Cache中;
S(Shared):這行資料有效,資料和記憶體中的資料一致,資料存在於很多Cache中;
I(Invalid):這行資料無效。
那麼,假設有一個變數i=3(應該是包括變數i的快取塊,塊大小為快取行大小);已經載入到多核(a,b,c)的快取中,此時該快取行的狀態為S;此時其中的一個核a改變了變數i的值,那麼在核a中的當前快取行的狀態將變為M,b,c核中的當前快取行狀態將變為I。如下圖:
偽共享問題
那麼為什麼會出現偽共享問題呢?上訴的情況再擴充套件一下,假設在多執行緒情況下,x,y兩個共享變數在同一個快取行中,核a修改變數x,會導致核b,核c中的x變數和y變數同時失效。
此時對於在核a上執行的執行緒,僅僅只是修改了了變數x,卻導致同一個快取行中的所有變數都無效,需要重新刷快取(並不一定代表每次都要從記憶體中重新載入,也有可能是從其他Cache中匯入資料,具體的實現要看各個晶片廠商的實現了)。
假設此時在核b上執行的執行緒,正好想要修改變數Y,那麼就會出現相互競爭,相互失效的情況,這就是偽共享啦。
Java對於偽共享的傳統解決方案
package com.alibaba;
/**
* Created by Administrator on 2016/10/13 0013.
*/
public final class FalseSharing implements Runnable {
private final static int NUM_THREADS = 4; // change
private final static long ITERATIONS = 500L * 1000L * 1000L;
private final int arrayIndex;
private static VolatileLong[] longs = new VolatileLong[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 {
final 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;
public long p1, p2, p3, p4, p5, p6;
}
}
執行結果:
duration = 9465942893
現在,我們將VolatileLong中不使用的6個long變數註釋掉,再次執行:
public final static class VolatileLong {
public volatile long value = 0L;
//public long p1, p2, p3, p4, p5, p6;
}
duration = 20362748888
可以看到,兩個程式邏輯完全一致,只是註釋掉了幾個沒有使用到的變數,卻導致效能相差很大。 我們知道一條快取行有64位元組, 而Java程式的物件頭固定佔8位元組(32位系統)或12位元組(64位系統預設開啟壓縮, 不開壓縮為16位元組). 我們只需要填6個無用的長整型補上6*8=48位元組, 讓不同的VolatileLong物件處於不同的快取行, 就可以避免偽共享了(64位系統超過快取行的64位元組也無所謂,只要保證不同執行緒不要操作同一快取行就可以)。這個辦法叫做補齊(Padding)。
Java8中的解決方案
Java8中已經提供了官方的解決方案,Java8中新增了一個註解:@sun.misc.Contended。加上這個註解的類會自動補齊快取行,需要注意的是此註解預設是無效的,需要在jvm啟動時設定-XX:-RestrictContended才會生效。
執行結果:
@sun.misc.Contended
public final static class VolatileLong {
public volatile long value = 0L;
//public long p1, p2, p3, p4, p5, p6;
}
duration = 8987991013
參考文獻:
1:http://igoro.com/archive/gallery-of-processor-cache-effects/
2:http://ifeve.com/false-sharing/
3:http://blog.csdn.net/muxiqingyang/article/details/6615199
相關文章
- JVM偽共享JVM
- 如何應對偽造的 SSL 證書?
- linux 下同步方案以及站點檔案的防篡改(偽)Linux
- CAS原子操作以及其在Java中的應用Java
- Java反射以及在Android中的特殊應用Java反射Android
- CSS偽元素詳解以及偽元素與偽類的區別CSS
- 關於對於Java中Entity以及VO,以及DTO中Request物件序列化的學習Java物件
- TDSQL | 在整個技術解決方案中HTAP對應的混合交易以及分析系統應該如何實現?SQL
- 解決方案| MongoDB PSA 架構痛點以及如何應對?MongoDB架構
- 精美圖文講解Java AQS 共享式獲取同步狀態以及Semaphore的應用JavaAQS
- Hbase master gone 系統崩潰. 遭遇 hbase bug 以及對應的解決方案.ASTGo
- JavaWeb 專案中的絕對路徑和相對路徑以及問題的解決方案JavaWeb
- Java多執行緒開發|volatile與偽共享問題Java執行緒
- 從同步原語看非阻塞同步以及Java中的應用Java
- 對Java Web中WEB-INF目錄的理解以及訪問方法JavaWeb
- 請問大家在java企業應用中功能url偽造如何防範的?Java
- 快取行競爭和偽共享快取
- 電腦科學中抽象的好處與問題—偽共享例項分析抽象
- web應用中通過偽列排序Web排序
- Java代理以及在Android中的一些簡單應用JavaAndroid
- Jetpack—LiveData元件的缺陷以及應對策略JetpackLiveData元件
- css3 中的偽類和偽元素CSSS3
- LNMP環境中WordPress程式偽靜態解決方案LNMP
- rownum偽列的應用
- JDBC踩坑──`tinyint(1)`預設對應Java中的`Boolean`JDBCJavaBoolean
- 淺談對CSRF的認識,以及一些應對措施
- java 中對物件的呼叫Java物件
- mysql 5.7 導致的行為改變. 效能優化的副產品 以及對應的解決方案MySql優化
- 雜談 什麼是偽共享(false sharing)?False
- CacheLine偽共享發現與優化優化
- Kubernetes中Pod間共享記憶體方案記憶體
- Java解析OFFICE(word,excel,powerpoint)以及PDF的實現方案及開發中的點滴分享JavaExcel
- 區塊鏈資訊共享應用落地搭建解決方案區塊鏈
- 應對分散式快取當機的方案分散式快取
- linux-HA 中heartbeat 的pg啟動指令碼的bug 及其應對方案。Linux指令碼
- 列印對應spid或sid所對應的sql以及其執行計劃SQL
- Oracle HRMS中的共享資訊以及使用者型別選擇的說明Oracle型別
- Maven中模組的聚合以及對jar包的繼承MavenJAR繼承