JVM系列之:Contend注解和false-sharing
文章目錄
- 簡介
- false-sharing的由來
- 怎么解決?
- 使用JOL分析
- Contended在JDK9中的問題
- padded和unpadded性能對比
- Contended在JDK中的使用
- 總結(jié)
簡介
現(xiàn)代CPU為了提升性能都會有自己的緩存結(jié)構(gòu),而多核CPU為了同時正常工作,引入了MESI,作為CPU緩存之間同步的協(xié)議。MESI雖然很好,但是不當(dāng)?shù)臅r候用也可能導(dǎo)致性能的退化。
到底怎么回事呢?一起來看看吧。
false-sharing的由來
為了提升處理速度,CPU引入了緩存的概念,我們先看一張CPU緩存的示意圖:
CPU緩存是位于CPU與內(nèi)存之間的臨時數(shù)據(jù)交換器,它的容量比內(nèi)存小的多但是交換速度卻比內(nèi)存要快得多。
CPU的讀實(shí)際上就是層層緩存的查找過程,如果所有的緩存都沒有找到的情況下,就是主內(nèi)存中讀取。
為了簡化和提升緩存和內(nèi)存的處理效率,緩存的處理是以Cache Line(緩存行)為單位的。
一次讀取一個Cache Line的大小到緩存。
在mac系統(tǒng)中,你可以使用sysctl machdep.cpu.cache.linesize來查看cache line的大小。
在linux系統(tǒng)中,使用getconf LEVEL1_DCACHE_LINESIZE來獲取cache line的大小。
本機(jī)中cache line的大小是64字節(jié)。
考慮下面一個對象:
public class CacheLine {public long a;public long b; }很簡單的對象,通過之前的文章我們可以指定,這個CacheLine對象的大小應(yīng)該是12字節(jié)的對象頭+8字節(jié)的long+8字節(jié)的long+4字節(jié)的補(bǔ)全,總共應(yīng)該是32字節(jié)。
因?yàn)?2字節(jié)< 64字節(jié),所以一個cache line就可以將其包括。
現(xiàn)在問題來了,如果是在多線程的環(huán)境中,thread1對a進(jìn)行累加,而thread2對b進(jìn)行累加。會發(fā)生什么情況呢?
大家注意,耗時點(diǎn)就在第4步。 雖然a和b是兩個不同的long,但是因?yàn)樗麄儽话谕粋€cache line中,最終導(dǎo)致了雖然兩個線程沒有共享同一個數(shù)值對象,但是還是發(fā)送了鎖的關(guān)聯(lián)情況。
怎么解決?
那怎么解決這個問題呢?
在JDK7之前,我們需要使用一些空的字段來手動補(bǔ)全。
public class CacheLine { public long actualValue; public long p0, p1, p2, p3, p4, p5, p6, p7; }像上面那樣,我們手動填充一些空白的long字段,從而讓真正的actualValue可以獨(dú)占一個cache line,就沒有這些問題了。
但是在JDK8之后,java文件的編譯期會將無用的變量自動忽略掉,那么上面的方法就無效了。
還好,JDK8中引入了sun.misc.Contended注解,使用這個注解會自動幫我們補(bǔ)全字段。
使用JOL分析
接下來,我們使用JOL工具來分析一下Contended注解的對象和不帶Contended注解的對象有什么區(qū)別。
@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參數(shù)。
同時可以設(shè)置-XX:ContendedPaddingWidth 來控制padding的大小。
INFO com.flydean.CacheLineJOL - com.flydean.CacheLine object internals:OFFSET SIZE TYPE DESCRIPTION VALUE0 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 024 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 VALUE0 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 024 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字節(jié)。直接填充了128字節(jié)。
Contended在JDK9中的問題
sun.misc.Contended是在JDK8中引入的,為了解決填充問題。
但是大家注意,Contended注解是在包sun.misc,這意味著一般來說是不建議我們直接使用的。
雖然不建議大家使用,但是還是可以用的。
但如果你使用的是JDK9-JDK14,你會發(fā)現(xiàn)sun.misc.Contended沒有了!
因?yàn)镴DK9引入了JPMS(Java Platform Module System),它的結(jié)構(gòu)跟JDK8已經(jīng)完全不一樣了。
經(jīng)過我的研究發(fā)現(xiàn),sun.misc.Contended, sun.misc.Unsafe,sun.misc.Cleaner這樣的類都被移到了jdk.internal.**中,并且是默認(rèn)不對外使用的。
那么有人要問了,我們換個引用的包名是不是就行了?
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 modulejava.base, which does not export it to the unnamed module)好,我們找到問題所在了,因?yàn)槲覀兊拇a并沒有定義module,所以是一個默認(rèn)的“unnamed” module,我們需要把java.base中的jdk.internal.vm.annotation使unnamed module可見。
要實(shí)現(xiàn)這個目標(biāo),我們可以在javac中添加下面的flag:
--add-exports java.base/jdk.internal.vm.annotation=ALL-UNNAMED好了,現(xiàn)在我們可以正常通過編譯了。
padded和unpadded性能對比
上面我們看到padded對象大小是160字節(jié),而unpadded對象的大小是32字節(jié)。
對象大了,運(yùn)行的速度會不慢呢?
實(shí)踐出真知,我們使用JMH工具在多線程環(huán)境中來對其進(jìn)行測試:
@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)@Benchmarkpublic long updateUnpaddedA() {return cacheLine.a++;}@Group("unpadded")@GroupThreads(1)@Benchmarkpublic long updateUnpaddedB() {return cacheLine.b++;}@Group("padded")@GroupThreads(1)@Benchmarkpublic long updatePaddedA() {return cacheLinePadded.a++;}@Group("padded")@GroupThreads(1)@Benchmarkpublic 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進(jìn)行累計(jì)操作,看下最后的運(yùn)行結(jié)果:
從結(jié)果看來雖然padded生成的對象比較大,但是因?yàn)锳和B在不同的cache line中,所以不會出現(xiàn)不同的線程去主內(nèi)存取數(shù)據(jù)的情況,因此要執(zhí)行的比較快。
Contended在JDK中的使用
其實(shí)Contended注解在JDK源碼中也有使用,不算廣泛,但是都很重要。
比如在Thread中的使用:
比如在ConcurrentHashMap中的使用:
其他使用的地方:Exchanger,ForkJoinPool,Striped64。
感興趣的朋友可以仔細(xì)研究一下。
總結(jié)
Contented從最開始的sun.misc到現(xiàn)在的jdk.internal.vm.annotation,都是JDK內(nèi)部使用的class,不建議大家在應(yīng)用程序中使用。
這就意味著我們之前使用的方式是不正規(guī)的,雖然能夠達(dá)到效果,但是不是官方推薦的。那么我們還有沒有什么正規(guī)的辦法來解決false-sharing的問題呢?
有知道的小伙伴歡迎留言給我討論!
本文作者:flydean程序那些事
本文鏈接:http://www.flydean.com/jvm-contend-false-sharing/
本文來源:flydean的博客
歡迎關(guān)注我的公眾號:程序那些事,更多精彩等著您!
總結(jié)
以上是生活随笔為你收集整理的JVM系列之:Contend注解和false-sharing的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 看动画学算法之:排序-快速排序
- 下一篇: JVM系列之:对象的锁状态和同步