偽共享和緩存行填充,Java并發編程還能這么優化!

前言

關于偽共享的文章已經很多了,對于多線程編程來說,特別是多線程處理列表和數組的時候,要非常注意偽共享的問題。否則不僅無法發揮多線程的優勢,還可能比單線程性能還差。隨著JAVA版本的更新,再各個版本上減少偽共享的做法都有區別,一不小心代碼可能就失效了,要注意進行測試。這篇文章總結一下。

什么是偽共享
關于偽共享講解最清楚的是這篇文章:http://developer.51cto.com/art/201306/398232.htm,我這里就直接摘抄其對偽共享的解釋:

緩存系統中是以緩存行(cache line)為單位存儲的。緩存行是2的整數冪個連續字節,一般為32-256個字節。最常見的緩存行大小是64個字節。當多線程修改互相獨立的變量時,如果這些變量共享同一個緩存行,就會無意中影響彼此的性能,這就是偽共享。緩存行上的寫競爭是運行在SMP系統中并行線程實現可伸縮性最重要的限制因素。有人將偽共享描述成無聲的性能殺手,因為從代碼中很難看清楚是否會出現偽共享。

為了讓可伸縮性與線程數呈線性關系,就必須確保不會有兩個線程往同一個變量或緩存行中寫。兩個線程寫同一個變量可以在代碼中發現。為了確定互相獨立的變量是否共享了同一個緩存行,就需要了解內存布局,或找個工具告訴我們。Intel VTune就是這樣一個分析工具。本文中我將解釋Java對象的內存布局以及我們該如何填充緩存行以避免偽共享。

image


圖1說明了偽共享的問題。在核心1上運行的線程想更新變量X,同時核心2上的線程想要更新變量Y。不幸的是,這兩個變量在同一個緩存行中。每個線程都要去競爭緩存行的所有權來更新變量。如果核心1獲得了所有權,緩存子系統將會使核心2中對應的緩存行失效。當核心2獲得了所有權然后執行更新操作,核心1就要使自己對應的緩存行失效。這會來來回回的經過L3緩存,大大影響了性能。如果互相競爭的核心位于不同的插槽,就要額外橫跨插槽連接,問題可能更加嚴重。

JAVA 6下的方案

解決偽共享的辦法是使用緩存行填充,使一個對象占用的內存大小剛好為64bytes或它的整數倍,這樣就保證了一個緩存行里不會有多個對象。這篇文章http://developer.51cto.com/art/201306/398232.htm提供了緩存行填充的例子:

public final 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]; 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; // comment out } 
}

VolatileLong通過填充一些無用的字段p1,p2,p3,p4,p5,p6,再考慮到對象頭也占用8bit, 剛好把對象占用的內存擴展到剛好占64bytes(或者64bytes的整數倍)。這樣就避免了一個緩存行中加載多個對象。但這個方法現在只能適應JAVA6 及以前的版本了。

(注:如果我們的填充使對象size大于64bytes,比如多填充16bytes– public long p1, p2, p3, p4, p5, p6, p7, p8;。理論上同樣應該避免偽共享問題,但事實是這樣的話執行速度同樣慢幾倍,只比沒有使用填充好一些而已。還沒有理解其原因。所以測試下來,必須是64bytes的整數倍)

JAVA 7下的方案
上面這個例子在JAVA 7下已經不適用了。因為JAVA 7會優化掉無用的字段,可以參考:http://ifeve.com/false-shareing-java-7-cn/。

因此,JAVA 7下做緩存行填充更麻煩了,需要使用繼承的辦法來避免填充被優化掉,這篇文章http://ifeve.com/false-shareing-java-7-cn/里的例子我覺得不是很好,于是我自己做了一些優化,使其更通用:

public final class FalseSharing implements Runnable {  public static int NUM_THREADS = 4; // change  public final static long ITERATIONS = 500L * 1000L * 1000L;  private final int arrayIndex;  private static VolatileLong[] longs;  public FalseSharing(final int arrayIndex) {  this.arrayIndex = arrayIndex;  }  public static void main(final String[] args) throws Exception {  Thread.sleep(10000);  System.out.println("starting....");  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();  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 class VolatileLongPadding {public volatile long p1, p2, p3, p4, p5, p6; // 注釋  
}public class VolatileLong extends VolatileLongPadding {public volatile long value = 0L;  
}

把padding放在基類里面,可以避免優化。(這好像沒有什么道理好講的,JAVA7的內存優化算法問題,能繞則繞)。不過,這種辦法怎么看都有點煩,借用另外一個博主的話:做個java程序員真難。

JAVA 8下的方案
在JAVA 8中,緩存行填充終于被JAVA原生支持了。JAVA 8中添加了一個@Contended的注解,添加這個的注解,將會在自動進行緩存行填充。以上的例子可以改為:

public final class FalseSharing implements Runnable {  public static int NUM_THREADS = 4; // change  public final static long ITERATIONS = 500L * 1000L * 1000L;  private final int arrayIndex;  private static VolatileLong[] longs;  public FalseSharing(final int arrayIndex) {  this.arrayIndex = arrayIndex;  }  public static void main(final String[] args) throws Exception {  Thread.sleep(10000);  System.out.println("starting....");  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();  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;  }  }  
}@Contended
public class VolatileLong {public volatile long value = 0L;  
}

執行時,必須加上虛擬機參數-XX:-RestrictContended,@Contended注釋才會生效。很多文章把這個漏掉了,那樣的話實際上就沒有起作用。

@Contended注釋還可以添加在字段上,今后再寫文章詳細介紹它的用法。

(后記:以上代碼基于32位JDK測試,64位JDK下,對象頭大小不同,有空再測試一下)

參考

http://mechanical-sympathy.blogspot.com/2011/07/false-sharing.html

http://mechanical-sympathy.blogspot.hk/2011/08/false-sharing-java-7.html

http://robsjava.blogspot.com/2014/03/what-is-false-sharing.html

原文發布時間為:2018-07-12
本文作者:Binhua
本文來自云棲社區合作伙伴“Java架構沉思錄”,了解相關信息可以關注“Java架構沉思錄”。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/451228.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/451228.shtml
英文地址,請注明出處:http://en.pswp.cn/news/451228.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

mysql中like % %模糊查詢

1&#xff0c;%&#xff1a;表示任意0個或多個字符。可匹配任意類型和長度的字符&#xff0c;有些情況下若是中文&#xff0c;請使用兩個百分號&#xff08;%%&#xff09;表示。 比如 SELECT * FROM [user] WHERE u_name LIKE %三% 將會把u_name為“張三”&#xff0c;“張貓三…

Java中判斷字符串是否為數字的五種方法

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 推薦使用第二個方法&#xff0c;速度最快。 方法一&#xff1a;用JAVA自帶的函數 Java代碼 public static boolean isNumeric(String…

慕學在線網0.4_xadmin后臺管理

admin是基于Django開發的后臺管理框架&#xff0c;方便&#xff0c;快捷&#xff0c;而且簡單&#xff1b;   而xadmin就相當于admin的升級版&#xff0c;更加強大。    1、安裝xadmin&#xff08;源碼安裝方式&#xff09;  教程 PS&#xff1a; - 卸載pip安裝的xadminp…

從一次換機器的過程談軟硬件的分離

今天把在公司使用的計算機更換了一臺&#xff0c;原來是Dell的780&#xff0c;換成了Dell的790&#xff0c;機箱的樣子變化比較大&#xff0c;但是里面硬件的配置變換并不大&#xff0c;最明顯的變化就在于CPU&#xff0c;其他像內存、硬盤等等的配置與原來的計算機基本上一致。…

知其所以然~redis的原子性

原子性 原子性是數據庫的事務中的特性。在數據庫事務的情景下&#xff0c;原子性指的是&#xff1a;一個事務&#xff08;transaction&#xff09;中的所有操作&#xff0c;要么全部完成&#xff0c;要么全部不完成&#xff0c;不會結束在中間某個環節。 對于Redis而言&#xf…

JoinPoint的用法

JoinPoint 對象 JoinPoint對象封裝了SpringAop中切面方法的信息,在切面方法中添加JoinPoint參數,就可以獲取到封裝了該方法信息的JoinPoint對象. 常用api: 方法名功能Signature getSignature();獲取封裝了署名信息的對象,在該對象中可以獲取到目標方法名,所屬類的Class等信息…

解決 No projects are available for deployment to this server!

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 如題&#xff0c;今天在嘗試部署從SVN上down下來的項目時&#xff0c;發現不能被tomcat識別成web項目&#xff01;原因是SVN上down下來的…

地大信工成果快報

在成果快報投稿中&#xff0c;請注意以下幾個問題&#xff1a;&#xff08;1&#xff09;成果信息一定要準確、全面&#xff0c;所有作者必須都要列出來&#xff0c;而不要出現et al. 這樣的表述&#xff0c;通訊作者一定要用*號標注&#xff1b;&#xff08;2&#xff09;成果…

javaBean的命名規則

前段時間&#xff0c;寫程序時&#xff0c;出了錯誤&#xff0c;竟然沒有想到是自己屬性命名的問題&#xff0c;哎~~~真是一定要注意規范呀&#xff0c;在這里我從網上找了些&#xff0c;規范作為參考 Sun 推薦的命名規范 1 &#xff0c;類名要首字母大寫&#xff0c;后面的單詞…

volatile的應用

volatile&#xff0c;中文意思是不穩定的、反復無常的&#xff0c;用來修飾變量&#xff0c;和多線程、并發有關系。 Java代碼在編譯后會變成Java字節碼&#xff0c;字節碼被類加載器加載到JVM里&#xff0c;JVM執行字節碼&#xff0c;最終需要轉化為匯編指令在CPU上執行。 在多…

漫談國內智能手機市場現狀

本文純屬一時興起&#xff0c;想到哪兒寫到哪兒&#xff0c;本人文筆也不咋地&#xff0c;寫的也比較隨意&#xff0c;如有錯誤歡迎指正&#xff0c;有啥意見歡迎交流。原創文章&#xff0c;轉載注明emouse的技術專欄。 我是一個不折不扣的數碼愛好者&#xff0c;對電腦手機這些…

【刷題】BZOJ 4195 [Noi2015]程序自動分析

Description 在實現程序自動分析的過程中,常常需要判定一些約束條件是否能被同時滿足。 考慮一個約束滿足問題的簡化版本&#xff1a;假設x1,x2,x3,…代表程序中出現的變量&#xff0c;給定n個形如xixj或xi≠xj的變量相等/不等的約束條件&#xff0c;請判定是否可以分別為每一個…

mysql 5.5 安裝配置方法圖文教程

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 回憶一下mysql 5.5 安裝配置方法&#xff0c;整理mysql 5.5 安裝配置教程筆記&#xff0c;分享給大家。 MySQL下載地址&#xff1a;htt…

git解除與遠程分支的關聯

在工作中&#xff0c;經常需要將同一份代碼傳到不同的git倉庫中去 如果本地同樣一份代碼&#xff0c;已經關聯了一個與遠程分支&#xff0c;那么怎么才能解除原程分支&#xff0c;并關聯到一個新的分支將代碼提交到新的分支上去呢&#xff1f; 1、如果你已經在遠程創建了一個分…

FindWindow用法

函數功能&#xff1a;該函數獲得一個頂層窗口的句柄&#xff0c;該窗口的類名和窗口名與給定的字符串相匹配。這個函數不查找子窗口。在查找時不區分大小寫。 函數型&#xff1a;HWND FindWindow&#xff08;LPCTSTR IpClassName&#xff0c;LPCTSTR IpWindowName&#xff0…

中國大城市政治地位綜合實力排名

中國大城市政治地位綜合實力排名&#xff01; 中國大城市政治地位綜合實力排名&#xff01;政治地位: 政治地位: 1&#xff08;直轄市 4 個&#xff09;&#xff1a;上海、北京、天津、重慶 2&#xff08;副省級城市 15 個&#xff09;&#xff1a;廣州、深圳、武漢、南京、沈陽…

sourcemap總結

sourcemap在線上壓縮文件調試中很重要&#xff0c;在此總結如下&#xff1a; 1. 開啟sourcemap (1). 瀏覽器要開啟source-map支持(2). 壓縮文件底部要有source-map的URL&#xff0c;壓縮要開啟source-map(3). .map文件要放在服務器&#xff0c;source-map URL指向的位置 2. sou…

navicat 導出的sql文件,再導入,運行SQL文件成功,數據庫中卻沒有表

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 問題描述&#xff1a;本來在數據庫上右鍵 &#xff0c;運行SQL文件 &#xff0c;就可以導入 sql ,建表成功&#xff0c;并且數據也該的…

mysql索引之二級索引學習總結

二級索引又稱輔助索引、非聚集索引(no-clustered index)。b&#xff0b;tree樹結構。然而二級索引的葉子節點不保存記錄中的所有列&#xff0c;其葉子節點保存的是<健值&#xff0c;(記錄)地址>。好似聚集索引中非葉子節點保存的信息&#xff0c;不同的是二級索引保存的是…

264,avs中Skip宏塊與Direct預測模式 ,對稱模式的區別

1. B_Skip類型宏塊 &#xff1a;無像素殘差&#xff0c;無運動矢量殘差&#xff08;MVD&#xff09;和參考幀。解碼時&#xff0c;通過Direct預測模式&#xff08;時間或空間&#xff09;計算出前、后向MV后&#xff0c;直接利用前、后向MV得到像素預測值。像 素重構值像…