简介
古代 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 进行累加。会产生什么状况呢?
- 第一步,新创建进去的对象被存储到 CPU1 和 CPU2 的缓存 cache line 中。
- thread1 应用 CPU1 对对象中的 a 进行累计。
- 依据 CPU 缓存之间的同步协定 MESI(这个协定比较复杂,这里就先不开展解说),因为 CPU1 对缓存中的 cache line 进行了批改,所以 CPU2 中的这个 cache line 的正本对象将会被标记为 I(Invalid)有效状态。
- 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 的博客
欢送关注我的公众号: 程序那些事,更多精彩等着您!