1. 程式人生 > >Java多執行緒_快取對齊

Java多執行緒_快取對齊

1.什麼是快取對齊

  當前的電腦中,資料儲存在磁碟上,可以斷電儲存,但是讀取效率較低。不斷電的情況下,資料可以在記憶體中儲存,相對硬碟效率差不多是磁碟的一萬倍左右。但是運算時,速度最快的是直接快取在CPU中的資料。CPU有三級快取分別是L1,L2,L3三級,CPU訪問速度大概是記憶體的100倍。

1.1CPU結構

  對於一臺電腦,其主機板可以支援多少個CPU插槽,稱為CPU個數。對於一顆多核CPU,單片CPU上整合的處理核心稱為CPU核數。對於每個核心,可以給每個核設定兩組暫存器,兩組pc。

 

 

 

  CPU結構如上圖所示(圖片來自網路),對於一塊CPU,可以有多個處理核心。每個核心內有自己的L1,L2快取,多個核心共用同一個L3快取。但一個電腦如果有多個CPU插槽,各個CPU有自己的L3。對於一個CPU核心來說,每個核心都有ALU,邏輯運算單元。負責對指令進行計算。Register 暫存器,記錄執行緒執行對應的資料。PC:指令暫存器,記錄執行緒執行到了哪個位置。裡面存的是指令行數。通俗講,就是記錄執行緒執行到了哪一行指令(程式碼在進入CPU執行前,會被編譯成指令)了。

  執行緒在執行的時候,將當前執行緒對應的資料放入暫存器,將執行行數放到指令暫存器,然後執行過一個時間片後,如果執行緒沒有執行完,將資料和指令儲存,然後其他執行緒進入執行。一個ALU對應多個PC|registers的時候(所謂的四核八執行緒)。一般來說,同一個CPU核在同一個時間點,只能執行同一個執行緒,但是,如果一個核裡面有兩組暫存器,兩個pc。那麼就可以同時執行兩組執行緒,在切換執行緒的時候,沒必要再去等待暫存器的資料儲存和資料載入。直接切換到下一組暫存器就可以。這就是超執行緒。

1.2快取對齊

 

  CPU到記憶體之間有很多層的記憶體,如圖所示,CPU需要經過L1,L2,L3及主記憶體才能讀到資料。從主記憶體讀取資料時的過程如下:

  當我左側的CPU讀取x的值的時候,首先會去L1快取中去找x的值,如果沒有,那麼取L2,L3依次去找。最後從主記憶體讀入的時候,首先將記憶體資料讀入L3,然後L2最後L1,然後再進行運算。但是讀取的時候,並不是只讀一個X的值,而是按塊去讀取(跟電腦的匯流排寬度有關,一次讀取一塊的資料,效率更高)。CPU讀取X後,很可能會用到相鄰的資料,所以在讀X的時候,會把同一塊中的Y資料也讀進來。這樣在用Y的時候,直接從L1中取資料就可以了。

  讀取的塊就叫做快取行,cache line 。快取行越大,區域性性空間效率越高,但讀取時間慢。快取行越小,區域性性空間效率越低,但讀取時間快。目前多取一個平衡的值,64位元組。

   然後,如果你的X和y在同一塊快取行中,且兩個欄位都用volatile修飾了,那麼將來兩個執行緒再修改的時候,就需要將x和y發生修改的訊息高速另外一個執行緒,讓它重新載入對應快取,然而另外一個執行緒並沒有使用該快取行中對應的內容,只是因為快取行讀取的時候跟變數相鄰,這就會產生效率問題。

  解決起來也簡單,我們將資料中的兩個volatile之間插入一些無用的記憶體,將第二個值擠出當前快取行,那麼執行的時候,就不會出現相應問題了。提高程式碼效率。

 

2.快取對齊在java中實現

  在java中,jdk一些涉及到多執行緒的類,有時候會看到類似於public volatile long p1,p2,p3,p4,p5,p6,p7;這樣的程式碼,有的就是做的快取行對齊。

  我們設計一個實驗去驗證快取行對齊的導致的效能問題,及相關的解決後的效率問題。具體程式碼見第三小節。這裡的思路是,首先,我們寫一個類T,這個類裡面有一個用volatile修飾的long屬性的值,這個值佔用8個位元組。然後宣告一個靜態陣列,包含兩個元素,分別T的兩個物件。然後開啟兩個執行緒,讓兩個執行緒分別給陣列的第一個值和第二個值賦值,執行一百萬次,看執行的耗時。

  這個時候,程式碼執行的時候如1.2的圖中所示,假設陣列中第一個值為X,第二個值為Y。左側框內為第一個執行緒,執行修改X值的操作,右側框內為第二個執行緒,修改Y的值。因為兩個值在同一個快取行中,所以在X值在讀取的時候,同時將X值和Y值一起讀入快取。第二個執行緒只修改Y的值,但是同樣將XY全部讀入快取。執行緒1中X值發生修改後,第二個執行緒中的X值需要進行更新。而執行緒2修改Y的值後也需要同樣的操作,但是這個更新不是必要的,而且會影響執行的效率。

  解決方法是:我們給第T的long值之前加入8個long值,這樣Y值就會被擠到其他快取行,這樣彼此修改的時候就不會產生干擾,提高程式碼執行效率。

  下面是具體驗證的程式碼,其中在沒有加入父類的時候,是相互干擾時的執行耗時。第二個是加入父類後,不再幹擾時的耗時,執行後可以看出,第二套程式碼在執行的時候,程式碼要優於第一套程式碼的執行。

3.快取對齊的程式碼實現

 1 public class T01_CacheLinePadding {
 2     private static class T{
 3         public volatile long x = 0L;
 4     }
 5     public static T[] orr = new T[2];
 6     static {
 7         orr[0]= new T();
 8         orr[1]= new T();
 9     }
10     public static void main(String[] args) throws Exception {
11         Thread t1 = new Thread(()->{
12             for (long i = 0; i < 1000_000L; i++) {
13                 orr[0].x = i;
14             }
15         });
16         Thread t2 = new Thread(()->{
17             for (long i = 0; i < 1000_000L; i++) {
18                 orr[1].x = i;
19             }
20         });
21         final long start = System.nanoTime();
22         t1.start();
23         t2.start();
24         t1.join();
25         t2.join();
26         System.out.println((System.nanoTime()-start)/100_000);
27     }
28 }
 1 package msb;
 2 /**
 3  * 快取行對齊問題程式碼
 4  * @author L Ys
 5  *
 6  */
 7 public class T02_CacheLinePadding {
 8     private static class Padding{
 9         public volatile long p1,p2,p3,p4,p5,p6,p7;
10     }
11     private static class T extends Padding{
12         public volatile long x = 0L;
13     }
14     public static T[] orr = new T[2];
15     static {
16         orr[0]= new T();
17         orr[1]= new T();
18     }
19     public static void main(String[] args) throws Exception {
20         Thread t1 = new Thread(()->{
21             for (long i = 0; i < 1000_000L; i++) {
22                 orr[0].x = i;
23             }
24         });
25         Thread t2 = new Thread(()->{
26             for (long i = 0; i < 1000_000L; i++) {
27                 orr[1].x = i;
28             }
29         });
30         final long start = System.nanoTime();
31         t1.start();
32         t2.start();
33         t1.join();
34         t2.join();
35         System.out.println((System.nanoTime()-start)/100_000);
36     }
37 }