JVM之緩存行對齊


緩存行

緩存是由多個緩存行組成的,以緩存行為基本單位,一個緩存行的大小一般為64字節。

偽共享

因為緩存行存在,當不同的線程在操作兩份不同的數據時,如果這兩份數據剛好位於同一個緩存行中,那么彼此之間就會互相影響。

假設A線程操作數據C,B線程操作數據D,C、D數據位於同一緩存行,那么當C數據發生修改時,由於緩存一致性協議的規定,就會造成緩存行失效,那么當B線程操作D數據時,就必須重新加載緩存,盡管B線程之前並沒有對D進行過任何操作,同理B線程的操作同樣會影響着A線程。

緩存行對齊

所以為了解決偽共享的問題,就出現了緩存行對齊的方式,也就是讓C、D兩份數據分別獨占一個緩存行,這樣就不會互相影響了。

如果要了解緩存,就必須要了解緩存的結構,以及多個CPU核心訪問緩存存在的一些問題和注意事項。

 

 

 

 

每個緩存里面都是由緩存行組成的,緩存系統中以緩存行(cache line)為單位存儲的。緩存行大小是64字節。由於緩存行的特性,當多線程修改互相獨立的變量時,如果這些變量共享同一個緩存行,就會無意中影響彼此的性能,這就是偽共享(下面會介紹到)。有人將偽共享描述成無聲的性能殺手,因為從代碼中很難看清楚是否會出現偽共享問題。

需要注意,數據在緩存中不是以獨立的項來存儲的,它不是我們認為的一個獨立的變量,也不是一個單獨的指針,它是有效引用主存中的一塊地址。一個Java的long類型是8字節,因此在一個緩存行中可以存8個long類型的變量。

 

 

 緩存行的這種特性也決定了在訪問同一緩存行中的數據時效率是比較高的。比如當你訪問java中的一個long類型的數組,當數組中的一個值被加載到緩存中,它會額外加載另外7個,因此可以非常快速的遍歷這個數組。實際上,你可以非常快速的遍歷在連續的內存塊中分配的任意數據結構。

 

處理器為了提高處理速度,不直接和內存進行通訊,而是先將系統內存的數據讀到內部緩存(L1,L2,L3)后再進行操作,但操作完之后不知道何時會寫到內存;如果對聲明了volatile 變量進行寫操作,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在的緩存行的數據寫回到系統內存。但就算寫回到內存,如果其他處理器緩存的值還是舊的,再執行計算操作就會有問題,所以在多處理器下,為了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器要對這個數據進行修改操作的時候,會強制重新從系統內存里把數據讀取到處理器緩存里。

為了說明偽共享問題,下面舉一個例子進行說明:兩個線程分別對兩個變量(剛好在同一個緩存航)分別進行讀寫的情況分析。

 

 

 

 

在core1上線程需要更新變量X,同時core2上線程需要更新變量Y。這種情況下,兩個變量就在同一個緩存行中。每個線程都要去競爭緩存行的所有權來更新對應的變量。如果core1獲得了緩存行的所有權,那么緩存子系統將會使core2中對應的緩存失效。相反,如果core2獲得了所有權然后執行更新操作,core1就要使自己對應的緩存行失效。這里需要注意:整個操作過程是以緩存行為單位進行處理的,這會來來回回的經過L3緩存,大大影響了性能,每次當前線程對緩存行進行寫操作時,內核都要把另一個內核上的緩存塊無效掉,並重新讀取里面的數據。如果相互競爭的核心位於不同的插槽,就要額外橫跨插槽連接,效率可能會更低。

 

緩存行對齊(實例代碼)

1:先看未使用緩存行對齊的方式

 1 package com.example.demo;
 2  
 3 public class Cacheline_nopadding {
 4     public static class T{
 5         //8字節
 6         private volatile long x = 0L;
 7     }
 8     private static T[] arr = new T[2];
 9  
10     static {
11         arr[0] = new T();
12         arr[1] = new T();
13     }
14  
15     public static void main(String[] args) throws InterruptedException {
16         Thread thread1 = new Thread(()->{
17             for(long i = 0;i < 1000_0000L;i++){
18                 //volatile的緩存一致性協議MESI或者鎖總線,會消耗時間
19                 arr[0].x = i;
20             }
21         });
22  
23         Thread thread2 = new Thread(()->{
24             for(long i = 0;i< 1000_0000L;i++){
25                 arr[1].x = i;
26             }
27         });
28         long startTime = System.nanoTime();
29         thread1.start();
30         thread2.start();
31         thread1.join();
32         thread2.join();
33         System.out.println("總計消耗時間:"+(System.nanoTime()-startTime)/100_000);
34     }
35 }

最終運行結果如下:

總計消耗時間:3381

下面來做一個改造升級,對齊緩存行,重點代碼如下

 

1    private static class Padding{
2         //7*8字節
3         public volatile long p1,p2,p3,p4,p5,p6,p7;
4     }
5     public static class T extends Padding{
6         //8字節
7         private volatile long x = 0L;
8     }

 

通過上述代碼做緩存對齊,每次都會有初始的7*8個占位,加上最后一個就是獨立的一塊緩存行,整理后代碼如下:

 1 package com.example.demo;
 2  
 3 public class Cacheline_padding {
 4     private static class Padding{
 5         //7*8字節
 6         public volatile long p1,p2,p3,p4,p5,p6,p7;
 7     }
 8     public static class T extends Padding{
 9         //8字節
10         private volatile long x = 0L;
11     }
12     private static T[] arr = new T[2];
13  
14     static {
15         arr[0] = new T();
16         arr[1] = new T();
17     }
18  
19     public static void main(String[] args) throws InterruptedException {
20         Thread thread1 = new Thread(()->{
21             for(long i = 0;i < 1000_0000L;i++){
22                 //volatile的緩存一致性協議MESI或者鎖總線,會消耗時間
23                 arr[0].x = i;
24             }
25         });
26  
27         Thread thread2 = new Thread(()->{
28             for(long i = 0;i< 1000_0000L;i++){
29                 arr[1].x = i;
30             }
31         });
32         long startTime = System.nanoTime();
33         thread1.start();
34         thread2.start();
35         thread1.join();
36         thread2.join();
37         System.out.println("總計消耗時間:"+(System.nanoTime()-startTime)/100_000);
38     }
39 }

運行結果如下:

總計消耗時間:1428

從上面可以看到,使用緩存對齊,相同操作情況下對齊后的時間比沒對齊的時間減少一半。

上面這種緩存行填充的方法在早期是比較流行的一種解決辦法,比較有名的Disruptor框架就采用了這種解決辦法提高性能,Disruptor是一個線程內通信框架,用於線程里共享數據。與LinkedBlockingQueue類似,提供了一個高速的生產者消費者模型,廣泛用於批量IO讀寫,在硬盤讀寫相關的程序中應用十分廣泛,Apache旗下的HBase、Hive、Storm等框架都有使用Disruptor。

4、JAVA8對偽共享的解決

進入到JAVA8后,官方已經提供了對偽共享的解決辦法,那就是sun.misc.Contended注解,有了這個注解解決偽共享就變得簡單多了

1   @sun.misc.Contended
2     public static class T{
3         //8字節
4         private volatile long x = 0L;
5     }

需要注意:默認情況下此注解是無效的,需要在JVM啟動時設置-XX:-RestrictContended。完整的代碼如下:

 

 

 

 1 package com.example.demo;
 2  
 3 public class Cacheline_nopadding {
 4     @sun.misc.Contended
 5     public static class T{
 6         //8字節
 7         private volatile long x = 0L;
 8     }
 9     private static T[] arr = new T[2];
10  
11     static {
12         arr[0] = new T();
13         arr[1] = new T();
14     }
15  
16     public static void main(String[] args) throws InterruptedException {
17         Thread thread1 = new Thread(()->{
18             for(long i = 0;i < 1000_0000L;i++){
19                 //volatile的緩存一致性協議MESI或者鎖總線,會消耗時間
20                 arr[0].x = i;
21             }
22         });
23  
24         Thread thread2 = new Thread(()->{
25             for(long i = 0;i< 1000_0000L;i++){
26                 arr[1].x = i;
27             }
28         });
29         long startTime = System.nanoTime();
30         thread1.start();
31         thread2.start();
32         thread1.join();
33         thread2.join();
34         System.out.println("總計消耗時間:"+(System.nanoTime()-startTime)/100_000);
35     }
36 }

 

可以看到,只是在沒有緩存對齊的代碼基礎上添加了一個注解而已,測試結果如下:

總計消耗時間:1289

 

本文原文出自https://blog.csdn.net/mofeizhi/article/details/106816026


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM