简介

古代CPU为了晋升性能都会有本人的缓存构造,而多核CPU为了同时失常工作,引入了MESI,作为CPU缓存之间同步的协定。MESI尽管很好,然而不当的时候用也可能导致性能的进化。

到底怎么回事呢?一起来看看吧。

false-sharing的由来

为了晋升处理速度,CPU引入了缓存的概念,咱们先看一张CPU缓存的示意图:

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字节。

思考上面一个对象:

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之前,咱们须要应用一些空的字段来手动补全。

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注解的对象有什么区别。

@Testpublic 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                          0Instance size: 32 bytesSpace 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                         0Instance size: 160 bytesSpace 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的博客

欢送关注我的公众号:程序那些事,更多精彩等着您!