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

簡介

現代CPU爲了提高性能都會有本身的緩存結構,而多核CPU爲了同時正常工做,引入了MESI,做爲CPU緩存之間同步的協議。MESI雖然很好,可是不當的時候用也可能致使性能的退化。java

到底怎麼回事呢?一塊兒來看看吧。linux

false-sharing的由來

爲了提高處理速度,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進行累加。會發生什麼狀況呢?

  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以前,咱們須要使用一些空的字段來手動補全。

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註解的對象有什麼區別。

@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字節。

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.**中,而且是默認不對外使用的。

那麼有人要問了,咱們換個引用的包名是否是就好了?

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和unpadded性能對比

上面咱們看到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中的使用

其實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的博客

歡迎關注個人公衆號:程序那些事,更多精彩等着您!

相關文章
相關標籤/搜索