現代CPU爲了提高性能都會有本身的緩存結構,而多核CPU爲了同時正常工做,引入了MESI,做爲CPU緩存之間同步的協議。MESI雖然很好,可是不當的時候用也可能致使性能的退化。java
到底怎麼回事呢?一塊兒來看看吧。linux
爲了提高處理速度,CPU引入了緩存的概念,咱們先看一張CPU緩存的示意圖:緩存
CPU緩存是位於CPU與內存之間的臨時數據交換器,它的容量比內存小的多可是交換速度卻比內存要快得多。多線程
CPU的讀實際上就是層層緩存的查找過程,若是全部的緩存都沒有找到的狀況下,就是主內存中讀取。jvm
爲了簡化和提高緩存和內存的處理效率,緩存的處理是以Cache Line(緩存行)爲單位的。工具
一次讀取一個Cache Line的大小到緩存。性能
在mac系統中,你可使用sysctl machdep.cpu.cache.linesize來查看cache line的大小。
在linux系統中,使用getconf LEVEL1_DCACHE_LINESIZE來獲取cache line的大小。
本機中cache line的大小是64字節。測試
考慮下面一個對象:ui
public class CacheLine { public long a; public long b; }
很簡單的對象,經過以前的文章咱們能夠指定,這個CacheLine對象的大小應該是12字節的對象頭+8字節的long+8字節的long+4字節的補全,總共應該是32字節。spa
由於32字節< 64字節,因此一個cache line就能夠將其包括。
如今問題來了,若是是在多線程的環境中,thread1對a進行累加,而thread2對b進行累加。會發生什麼狀況呢?
你們注意,耗時點就在第4步。 雖然a和b是兩個不一樣的long,可是由於他們被包含在同一個cache line中,最終致使了雖然兩個線程沒有共享同一個數值對象,可是仍是發送了鎖的關聯狀況。
那怎麼解決這個問題呢?
在JDK7以前,咱們須要使用一些空的字段來手動補全。
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工具來分析一下Contended註解的對象和不帶Contended註解的對象有什麼區別。
@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的大小。
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
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字節。
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.**中,而且是默認不對外使用的。
那麼有人要問了,咱們換個引用的包名是否是就好了?
import jdk.internal.vm.annotation.Contended;
抱歉仍是不行。
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:
--add-exports java.base/jdk.internal.vm.annotation=ALL-UNNAMED
好了,如今咱們能夠正常經過編譯了。
上面咱們看到padded對象大小是160字節,而unpadded對象的大小是32字節。
對象大了,運行的速度會不慢呢?
實踐出真知,咱們使用JMH工具在多線程環境中來對其進行測試:
@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進行累計操做,看下最後的運行結果:
從結果看來雖然padded生成的對象比較大,可是由於A和B在不一樣的cache line中,因此不會出現不一樣的線程去主內存取數據的狀況,所以要執行的比較快。
其實Contended註解在JDK源碼中也有使用,不算普遍,可是都很重要。
好比在Thread中的使用:
好比在ConcurrentHashMap中的使用:
其餘使用的地方:Exchanger,ForkJoinPool,Striped64。
感興趣的朋友能夠仔細研究一下。
Contented從最開始的sun.misc到如今的jdk.internal.vm.annotation,都是JDK內部使用的class,不建議你們在應用程序中使用。
這就意味着咱們以前使用的方式是不正規的,雖然可以達到效果,可是不是官方推薦的。那麼咱們還有沒有什麼正規的辦法來解決false-sharing的問題呢?
有知道的小夥伴歡迎留言給我討論!
本文做者:flydean程序那些事本文連接:http://www.flydean.com/jvm-contend-false-sharing/
本文來源:flydean的博客
歡迎關注個人公衆號:程序那些事,更多精彩等着您!