1. 程式人生 > >JVM系列之:Contend註解和false-sharing

JVM系列之:Contend註解和false-sharing

[toc] # 簡介 現代CPU為了提升效能都會有自己的快取結構,而多核CPU為了同時正常工作,引入了MESI,作為CPU快取之間同步的協議。MESI雖然很好,但是不當的時候用也可能導致效能的退化。 到底怎麼回事呢?一起來看看吧。 # false-sharing的由來 為了提升處理速度,CPU引入了快取的概念,我們先看一張CPU快取的示意圖: ![](https://img-blog.csdnimg.cn/20200619150442477.png) CPU快取是位於CPU與記憶體之間的臨時資料交換器,它的容量比記憶體小的多但是交換速度卻比記憶體要快得多。 CPU的讀實際上就是層層快取的查詢過程,如果所有的快取都沒有找到的情況下,就是主記憶體中讀取。 為了簡化和提升快取和記憶體的處理效率,快取的處理是以Cache Line(快取行)為單位的。 一次讀取一個Cache Line的大小到快取。 > 在mac系統中,你可以使用sysctl machdep.cpu.cache.linesize來檢視cache line的大小。 > 在linux系統中,使用getconf LEVEL1_DCACHE_LINESIZE來獲取cache line的大小。 本機中cache line的大小是64位元組。 考慮下面一個物件: ~~~java public class CacheLine { public long a; public long b; } ~~~ 很簡單的物件,通過之前的文章我們可以指定,這個CacheLine物件的大小應該是12位元組的物件頭+8位元組的long+8位元組的long+4位元組的補全,總共應該是32位元組。 因為32位元組< 64位元組,所以一個cache line就可以將其包括。 現在問題來了,如果是在多執行緒的環境中,thread1對a進行累加,而thread2對b進行累加。會發生什麼情況呢? 1. 第一步,新創建出來的物件被儲存到CPU1和CPU2的快取cache line中。 2. thread1使用CPU1對物件中的a進行累計。 3. 根據CPU快取之間的同步協議MESI(這個協議比較複雜,這裡就先不展開講解),因為CPU1對快取中的cache line進行了修改,所以CPU2中的這個cache line的副本物件將會被標記為I(Invalid)無效狀態。 4. thread2使用CPU2對物件中的b進行累加,這個時候因為CPU2中的cache line已經被標記為無效了,所以必須重新從主記憶體中同步資料。 大家注意,耗時點就在第4步。 雖然a和b是兩個不同的long,但是因為他們被包含在同一個cache line中,最終導致了雖然兩個執行緒沒有共享同一個數值物件,但是還是傳送了鎖的關聯情況。 ## 怎麼解決? 那怎麼解決這個問題呢? 在JDK7之前,我們需要使用一些空的欄位來手動補全。 ~~~java public class CacheLine { public long actualValue; public long p0, p1, p2, p3, p4, p5, p6, p7; } ~~~ 像上面那樣,我們手動填充一些空白的long欄位,從而讓真正的actualValue可以獨佔一個cache line,就沒有這些問題了。 但是在JDK8之後,java檔案的編譯期會將無用的變數自動忽略掉,那麼上面的方法就無效了。 還好,JDK8中引入了sun.misc.Contended註解,使用這個註解會自動幫我們補全欄位。 # 使用JOL分析 接下來,我們使用JOL工具來分析一下Contended註解的物件和不帶Contended註解的物件有什麼區別。 ~~~java @Test public void useJol() { log.info("{}", ClassLayout.parseClass(CacheLine.class).toPrintable()); log.info("{}", ClassLayout.parseInstance(new CacheLine()).toPrintable()); log.info("{}", ClassLayout.parseClass(CacheLinePadded.class).toPrintable()); log.info("{}", ClassLayout.parseInstance(new CacheLinePadded()).toPrintable()); } ~~~ > 注意,在使用JOL分析Contended註解的物件時候,需要加上 -XX:-RestrictContended引數。 > > 同時可以設定-XX:ContendedPaddingWidth 來控制padding的大小。 ~~~java INFO com.flydean.CacheLineJOL - com.flydean.CacheLine object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) d0 29 17 00 (11010000 00101001 00010111 00000000) (1518032) 12 4 (alignment/padding gap) 16 8 long CacheLine.valueA 0 24 8 long CacheLine.valueB 0 Instance size: 32 bytes Space losses: 4 bytes internal + 0 bytes external = 4 bytes total ~~~ ~~~java INFO com.flydean.CacheLineJOL - com.flydean.CacheLinePadded object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) d2 5d 17 00 (11010010 01011101 00010111 00000000) (1531346) 12 4 (alignment/padding gap) 16 8 long CacheLinePadded.b 0 24 128 (alignment/padding gap) 152 8 long CacheLinePadded.a 0 Instance size: 160 bytes Space losses: 132 bytes internal + 0 bytes external = 132 bytes total ~~~ 我們看到使用了Contended的物件大小是160位元組。直接填充了128位元組。 # Contended在JDK9中的問題 sun.misc.Contended是在JDK8中引入的,為了解決填充問題。 但是大家注意,Contended註解是在包sun.misc,這意味著一般來說是不建議我們直接使用的。 雖然不建議大家使用,但是還是可以用的。 但如果你使用的是JDK9-JDK14,你會發現sun.misc.Contended沒有了! 因為JDK9引入了JPMS(Java Platform Module System),它的結構跟JDK8已經完全不一樣了。 經過我的研究發現,sun.misc.Contended, sun.misc.Unsafe,sun.misc.Cleaner這樣的類都被移到了jdk.internal.**中,並且是預設不對外使用的。 那麼有人要問了,我們換個引用的包名是不是就行了? ~~~java import jdk.internal.vm.annotation.Contended; ~~~ 抱歉還是不行。 ~~~java error: package jdk.internal.vm.annotation is not visible @jdk.internal.vm.annotation.Contended ^ (package jdk.internal.vm.annotation is declared in module java.base, which does not export it to the unnamed module) ~~~ 好,我們找到問題所在了,因為我們的程式碼並沒有定義module,所以是一個預設的“unnamed” module,我們需要把java.base中的jdk.internal.vm.annotation使unnamed module可見。 要實現這個目標,我們可以在javac中新增下面的flag: ~~~java --add-exports java.base/jdk.internal.vm.annotation=ALL-UNNAMED ~~~ 好了,現在我們可以正常通過編譯了。 # padded和unpadded效能對比 上面我們看到padded物件大小是160位元組,而unpadded物件的大小是32位元組。 物件大了,執行的速度會不慢呢? 實踐出真知,我們使用JMH工具在多執行緒環境中來對其進行測試: ~~~java @State(Scope.Benchmark) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @Fork(value = 1, jvmArgsPrepend = "-XX:-RestrictContended") @Warmup(iterations = 10) @Measurement(iterations = 25) @Threads(2) public class CacheLineBenchMark { private CacheLine cacheLine= new CacheLine(); private CacheLinePadded cacheLinePadded = new CacheLinePadded(); @Group("unpadded") @GroupThreads(1) @Benchmark public long updateUnpaddedA() { return cacheLine.a++; } @Group("unpadded") @GroupThreads(1) @Benchmark public long updateUnpaddedB() { return cacheLine.b++; } @Group("padded") @GroupThreads(1) @Benchmark public long updatePaddedA() { return cacheLinePadded.a++; } @Group("padded") @GroupThreads(1) @Benchmark public long updatePaddedB() { return cacheLinePadded.b++; } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(CacheLineBenchMark.class.getSimpleName()) .build(); new Runner(opt).run(); } } ~~~ 上面的JMH程式碼中,我們使用兩個執行緒分別對A和B進行累計操作,看下最後的執行結果: ![](https://img-blog.csdnimg.cn/20200619143042756.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 從結果看來雖然padded生成的物件比較大,但是因為A和B在不同的cache line中,所以不會出現不同的執行緒去主記憶體取資料的情況,因此要執行的比較快。 # Contended在JDK中的使用 其實Contended註解在JDK原始碼中也有使用,不算廣泛,但是都很重要。 比如在Thread中的使用: ![](https://img-blog.csdnimg.cn/20200619105903165.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 比如在ConcurrentHashMap中的使用: ![](https://img-blog.csdnimg.cn/20200619105915447.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 其他使用的地方:Exchanger,ForkJoinPool,Striped64。 感興趣的朋友可以仔細研究一下。 # 總結 Contented從最開始的sun.misc到現在的jdk.internal.vm.annotation,都是JDK內部使用的class,不建議大家在應用程式中使用。 這就意味著我們之前使用的方式是不正規的,雖然能夠達到效果,但是不是官方推薦的。那麼我們還有沒有什麼正規的辦法來解決false-sharing的問題呢? 有知道的小夥伴歡迎留言給我討論! > 本文作者:flydean程式那些事 > > 本文連結:[http://www.flydean.com/jvm-contend-false-sharing/](http://www.flydean.com/jvm-contend-false-sharing/) > > 本文來源:flydean的部落格 > > 歡迎關注我的公眾號:程式那些事,更多精彩等著您!