僞共享 false sharing,顧名思義,「僞共享」就是「其實不是共享」。那什麼是「共享」?多CPU同時訪問同一塊內存區域就是「共享」,就會產生衝突,須要控制協議來協調訪問。會引發「共享」的最小內存區域大小就是一個cache line。所以,當兩個以上CPU都要訪問同一個cache line大小的內存區域時,就會引發衝突,這種狀況就叫「共享」。可是,這種狀況裏面又包含了「其實不是共享」的「僞共享」狀況。好比,兩個處理器各要訪問一個word,這兩個word卻存在於同一個cache line大小的區域裏,這時,從應用邏輯層面說,這兩個處理器並無共享內存,由於他們訪問的是不一樣的內容(不一樣的word)。可是由於cache line的存在和限制,這兩個CPU要訪問這兩個不一樣的word時,卻必定要訪問同一個cache line塊,產生了事實上的「共享」。顯然,因爲cache line大小限制帶來的這種「僞共享」是咱們不想要的,會浪費系統資源。html
緩存系統中是以緩存行(cache line)爲單位存儲的。緩存行是2的整數冪個連續字節,通常爲32-256個字節。最多見的緩存行大小是64個字節。當多線程修改互相獨立的變量時,若是這些變量共享同一個緩存行,就會無心中影響彼此的性能,這就是僞共享。緩存行上的寫競爭是運行在SMP系統中並行線程實現可伸縮性最重要的限制因素。有人將僞共享描述成無聲的性能殺手,由於從代碼中很難看清楚是否會出現僞共享。java
爲了讓可伸縮性與線程數呈線性關係,就必須確保不會有兩個線程往同一個變量或緩存行中寫。兩個線程寫同一個變量能夠在代碼中發現。爲了肯定互相獨立的變量是否共享了同一個緩存行,就須要瞭解內存佈局,或找個工具告訴咱們。Intel VTune就是這樣一個分析工具。算法
圖1說明了僞共享的問題。在覈心1上運行的線程想更新變量X,同時核心2上的線程想要更新變量Y。不幸的是,這兩個變量在同一個緩存行中。每一個線程都要去競爭緩存行的全部權來更新變量。若是核心1得到了全部權,緩存子系統將會使核心2中對應的緩存行失效。當核心2得到了全部權而後執行更新操做,核心1就要使本身對應的緩存行失效。這會來來回回的通過L3緩存,大大影響了性能。若是互相競爭的核心位於不一樣的插槽,就要額外橫跨插槽鏈接,問題可能更加嚴重。數組
Java Memory Layout Java內存佈局,在項目開發中,大多使用HotSpot的JVM,hotspot中對象都有兩個字(四字節)長的對象頭。第一個字是由24位哈希碼和8位標誌位(如鎖的狀態或做爲鎖對象)組成的Mark Word。第二個字是對象所屬類的引用。若是是數組對象還須要一個額外的字來存儲數組的長度。每一個對象的起始地址都對齊於8字節以提升性能。所以當封裝對象的時候爲了高效率,對象字段聲明的順序會被重排序成下列基於字節大小的順序:緩存
在瞭解這些以後,就能夠在任意字段間用7個long來填充緩存行。僞共享在不一樣的JDK下提供了不一樣的解決方案。多線程
在JDK1.6環境下,解決僞共享的辦法是使用緩存行填充,使一個對象佔用的內存大小恰好爲64bytes或它的整數倍,這樣就保證了一個緩存行裏不會有多個對象。併發
package basic; public class TestFlash implements Runnable { public final static int NUM_THREADS = 4; // change public final static long ITERATIONS = 500L * 1000L * 1000L; private final int arrayIndex; /** * 爲了展現其性能影響,咱們啓動幾個線程,每一個都更新它本身獨立的計數器。計數器是volatile long類型的,因此其它線程能看到它們的進展。 */ public final static class VolatileLong { /* 用volatile[ˈvɑ:lətl]修飾的變量,線程在每次使用變量的時候,JVM虛擬機只保證從主內存加載到線程工做內存的值是最新的 */ public volatile long value = 0L; /* 緩衝行填充 */ /* 37370571461 :不使用緩衝行執行納秒數 */ /* 16174480826 :使用緩衝行執行納秒數,性能提升一半 */ public long p1, p2, p3, p4, p5, p6, p7; } private static VolatileLong[] longs = new VolatileLong[NUM_THREADS]; static { for (int i = 0; i < longs.length; i++) { longs[i] = new VolatileLong(); } } public TestFlash(final int arrayIndex){ this.arrayIndex = arrayIndex; } /** * 咱們不能肯定這些VolatileLong會佈局在內存的什麼位置。它們是獨立的對象。可是經驗告訴咱們同一時間分配的對象趨向集中於一塊。 */ 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 TestFlash(i)); } for (Thread t : threads) { t.start(); } for (Thread t : threads) { t.join(); } } /* * 爲了展現其性能影響,咱們啓動幾個線程,每一個都更新它本身獨立的計數器。計數器是volatile long類型的,因此其它線程能看到它們的進展 */ @Override public void run() { long i = ITERATIONS + 1; while (0 != --i) { longs[arrayIndex].value = i; } } }
VolatileLong經過填充一些無用的字段p1,p2,p3,p4,p5,p6,再考慮到對象頭也佔用8bit, 恰好把對象佔用的內存擴展到恰好佔64bytes(或者64bytes的整數倍)。這樣就避免了一個緩存行中加載多個對象。但這個方法如今只能適應JAVA6 及之前的版本了。jvm
在jdk1.7環境下,因爲java 7會優化掉無用的字段。所以,JAVA 7下作緩存行填充更麻煩了,須要使用繼承的辦法來避免填充被優化掉。把填充放在基類裏面,能夠避免優化(這好像沒有什麼道理好講的,JAVA7的內存優化算法問題,能繞則繞)。ide
package basic; public class TestFlashONJDK7 implements Runnable { public static int NUM_THREADS = 4; public final static long ITERATIONS = 500L * 1000L * 1000L; private final int arrayIndex; private static VolatileLong[] longs; public TestFlashONJDK7(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 TestFlashONJDK7(i)); } for (Thread t : threads) { t.start(); } for (Thread t : threads) { t.join(); } } @Override public void run() { long i = ITERATIONS + 1; while (0 != --i) { longs[arrayIndex].value = i; } } } class VolatileLong extends VolatileLongPadding { public volatile long value = 0L; } class VolatileLongPadding { public volatile long p1, p2, p3, p4, p5, p6, p7; }
在jdk1.8環境下,緩存行填充終於被JAVA原生支持了。JAVA 8中添加了一個@Contended的註解,添加這個的註解,將會在自動進行緩存行填充。以上的例子能夠改成:工具
package basic; public class TestFlashONJDK8 implements Runnable { public static int NUM_THREADS = 4; public final static long ITERATIONS = 500L * 1000L * 1000L; private final int arrayIndex; private static VolatileLong[] longs; public TestFlashONJDK8(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 TestFlashONJDK8(i)); } for (Thread t : threads) { t.start(); } for (Thread t : threads) { t.join(); } } @Override public void run() { long i = ITERATIONS + 1; while (0 != --i) { longs[arrayIndex].value = i; } } }
@Contended
class VolatileLong {
public volatile long value = 0L;
}
執行時,必須加上虛擬機參數-XX:-RestrictContended,@Contended註釋纔會生效。不少文章把這個漏掉了,那樣的話實際上就沒有起做用。
補充:
byte字節 bit位 1byte=8bit
volatile說明
package basic; public class TestVolatile { public static int count = 0; /* 即便使用volatile,依舊沒有達到咱們指望的效果 */ // public volatile static int count = 0; public static void increase() { try { // 延遲10毫秒,使得結果明顯 Thread.sleep(10); count++; } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { for (int i = 0; i < 10000; i++) { new Thread(new Runnable() { @Override public void run() { TestVolatile.increase(); } }).start(); } System.out.println("指望運行結果:10000"); System.out.println("實際運行結果:" + TestVolatile.count); } }
volatile關鍵字的使用:用volatile修飾的變量,線程在每次使用變量的時候,都會讀取變量修改後的最新值。可是因爲操做不是原子性的,對於volatile修飾的變量,jvm虛擬機只是保證從主內存加載到線程工做內存的值是最新的。
在java 垃圾回收整理一文中,描述了jvm運行時刻內存的分配。其中有一個內存區域是jvm虛擬機棧,每個線程運行時都有一個線程棧,線程棧保存了線程運行時候變量值信息。當線程訪問某一個對象時候值的時候,首先經過對象的引用找到對應在堆內存的變量的值,而後把堆內存變量的具體值load到線程本地內存中,創建一個變量副本,以後線程就再也不和對象在堆內存變量值有任何關係,而是直接修改副本變量的值,在修改完以後的某一個時刻(線程退出以前),自動把線程變量副本的值回寫到對象在堆中變量。這樣在堆中的對象的值就產生變化了。上面一幅圖描述這些交互,過程以下:
可是這些操做並非原子性,也就是在read load以後,若是主內存count變量發生修改以後,線程工做內存中的值因爲已經加載,不會產生對應的變化,因此計算出來的結果會和預期不同。對於volatile修飾的變量,JVM虛擬機只是保證從主內存加載到線程工做內存的值是最新的。例如假如線程1,線程2在進行read load操做中,發現主內存中count的值都是5,那麼都會加載這個最新的值。在線程1堆count進行修改以後,會write到主內存中,主內存中的count變量就會變爲6。線程2因爲已經進行read,load操做,在進行運算以後,也會更新主內存count的變量值爲6。致使兩個線程即便使用volatile關鍵字修改以後,仍是會存在併發的狀況。
對於volatile修飾的變量,JVM虛擬機只能保證從主內存加載到線程工做內存的值是最新的。
參考博客:
[1] http://www.cnblogs.com/Binhua-Liu/p/5620339.html