先看一個小程序,2個線程同時對數組array的第1個,第2個元素進行修改,每個線程修改1千萬次。
public class Cacheline_notPadding { public static class T { private volatile long x = 0L;// 占8字節 } private static T[] array = new T[2]; static { array[0] = new T(); array[1] = new T(); } public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(() -> { for (long i = 0; i < 1000_0000L; i++) { array[0].x = i;// 偽共享問題+緩存一致性協議在修改數據時會消耗額外的時間 } }); Thread thread2 = new Thread(() -> { for (long i = 0; i < 1000_0000L; i++) { array[1].x = i;// 偽共享問題+緩存一致性協議在修改數據時會消耗額外的時間 } }); long startTime = System.nanoTime(); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("總計消耗時間:" + (System.nanoTime() - startTime) / 100_000); } }
執行該小程序,總計消耗時間為:2565。實際上,該小程序存在一個細節問題,是可以進行優化的。這個細節問題就是緩存行偽共享問題。
緩存行偽共享問題
眾所周知,cpu將數據加載到緩存中的最小數據單位是行,緩存中也是以緩存行為單位進行存儲的。緩存行的大小一般為32-256個字節,最常見的緩存行大小是64個字節(本文中的示例環境中的緩存行大小為64個字節)。緩存行的容量限制帶來了一個問題,就是偽共享問題。如下圖所示,在本文的小程序中,我們假設線程thread1,thread2分別修改的是緩存C1,C2中的array[0],array[1]。雖然線程thread1只是修改array[0],但是因為緩存行的容量是64字節,而new T()中只有一個占8字節的屬性x,所以C1中的array[0]所在的緩存行在加載時也加載了array[1];C2中的array[1]所在的緩存行也是同理。但是實際上C1中的array所在的緩存行在計算時是不需要array[1]的,C2中的array所在的緩存行在計算時也不需要array[0]。這樣在緩存一致性協議作用下(這里以MESI協議為例),當線程thread1修改了C1中的array[0],那么勢必會通過總線通知C2作廢array[1]所在的緩存行,線程thread2修改array[1]時也是如此,緩存一致性協議所帶來的操作勢必會帶來額外的性能消耗。
緩存行對齊
那么怎么解決由於緩存行偽共享+緩存一致性協議帶來的額外的性能消耗呢?答案就是“緩存行對齊”。如下圖所示,如果讓緩存C1中的array[0]及C2中的array[1]各占一個緩存行,那么在計算時就互不影響了。
針對本文的小程序,采用緩存行對齊優化后的代碼如下:
在類T中,除了成員屬性x(占8個字節),再定義無任何使用意義的7個long類型的成員屬性p1...p7(占56個字節),這樣就會讓一個T對象至少占滿8+56=64個字節,這樣array每個元素所在的緩存行只能容下一個T對象了,由於array中的兩個元素各自獨占一個緩存行,那么線程thread1和thread2在計算時就不會互相影響了。
public class Cacheline_Padding { public static class T { private long p1, p2, p3, p4, p5, p6, p7;// 占7*8字節 緩存行對齊 private long x = 0L;// 占8字節 } private static T[] array = new T[2]; static { array[0]=new T(); array[1]=new T(); } public static void main(String[]args)throws InterruptedException{ Thread thread1=new Thread(()->{ for(long i=0;i< 1000_0000L;i++){ array[0].x=i;// array[0]獨占一個緩存行 } }); Thread thread2=new Thread(()->{ for(long i=0;i< 1000_0000L;i++){ array[1].x=i;// array[1]獨占一個緩存行 } }); long startTime=System.nanoTime(); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("總計消耗時間:"+(System.nanoTime()-startTime)/100_000); } }
執行程序,總計消耗時間:99
實際上,本文這樣定義無實際使用意義的成員屬性來達到緩存行對齊的方式在一些框架源碼中是有運用的,如在JDK7的LinkedBlockingQueue源碼及Disruptor框架。
緩存行對齊的其他方式
到了JDK8,對於緩存行對齊有了一種更加優雅的解決方式,那就是sun.misc.Contended注解,這個注解直接在類上定義就可以了。
@sun.misc.Contended// 緩存行對齊 每個T對象占64字節 public static class T { private long x = 0L; }
注意:如果此注解無效,需要在JVM啟動時設置-XX:-RestrictContended。