缓存行填充与@sun.misc.Contended注解

本文深入探讨了CPU缓存的结构与工作原理,包括L1、L2、L3缓存的区别,缓存行的概念及其对性能的影响,以及如何通过缓存一致性协议和伪共享解决方案来提升多核系统的效率。

1.缓存模型

CPU和主内存之间有好几层缓存,因为与cpu的速度相比,访问主内存的速度是非常慢的。如果频繁对同一个数据做运算,每次都从内存中加载,运算完之后再写回到主内存中,将会严重拖累cpu的计算资源。因此,为了充分发挥CPU的计算性能和吞吐量,平衡CPU和主内存之间的速度差距,现代CPU引入了一级缓存、二级缓存和三级缓存,结构如下图所示:

越靠近CPU的缓存存储速度越快,但是容量也越小。所以一级缓存(L1 Cache)最小最快,并且紧靠着在使用它的CPU内核。L2大一些,也慢一些,并且仍然只能被一个单独的 CPU 核使用。L3在现代多核机器中更普遍,仍然更大,更慢,并且被单个插槽上的所有 CPU 核共享。最后,你拥有一块主存,由全部插槽上的所有 CPU 核共享。

当CPU运算过程中需要加载数据时,首先去L1去寻找需要的数据,如果没有则去L2寻找,接着从L3中寻找,如果都没有,则从内存中读取数据。所以,如果某些数据需要经常被访问,那么这些数据存放在L1中的效率会最高。

而一般缓存未命中消耗的时钟周期及时间数据如下:

2.缓存行Cache Line

缓存是由缓存行组成的。一个缓存行存储字节的数量为2的倍数,在不同的机器上,缓存行大小为32字节到256字节不等,目前通常为64字节。

缓存行是按块加载到缓存中,一次性从内存中加载或者写入64字节的内容。假如一个Java对象有8个long类型的成员变量,那么一个缓存行最多可以一次性加载这8个long类型的变量到内存中。那么cpu在运算中,如果需要这8个变量,就可以直接从缓存中读取数据,而不需要从主内存中加载。

3.缓存一致性协议

由于一级缓存和二级缓存是被CPU核单独拥有的,当CPU核修改缓存中的内容时,缓存需要写回主内存,然后通知其他CPU核中对该缓存块进行失效处理。例如Core1和Core2并发读写同一个对象,该对象所属的内存地址块,会被同时缓存到一级缓存中。当Core1修改这个对象的变量时,改动会写回主内存,并且会让Core2缓存的该对象的缓存行失效。

4.伪共享(False Sharing)

伪共享,翻译为错误的共享。下面的图说明了伪共享的问题:

1.假设Core1更新了X值,那么缓存中的缓存行和内存中的值都会被改变,并且Core2的缓存行也都会失效,因为它缓存的X不是最新值了。那么Core2仅仅只是想读X值,却需要重新从主内存去加载,从而被缓存未命中拖慢了读取速度。

2.假设在核心1上运行的线程想更新变量X,同时核心2上的线程想要更新变量Y, 而这两个变量在同一个缓存行中。每个线程都要去竞争缓存行的所有权来更新变量。如果Core1获得了所有权,改变了X值,那么Core2中对应的缓存行失效。而Core2去读取Y时,发现缓存未命中,需要重新读取缓存行。然后当Core2去更新Y值时,也会使Core1的缓存行失效,从而使Core1引发一次缓存未命中。这会来来回回的经过L3缓存,大大影响了性能。如果互相竞争的核心位于不同的插槽,就要额外横跨插槽连接,问题可能更加严重。

5.伪共享解决办法

1.缓存行填充

disrupter框架通过增加字段,补全缓存行来确保ring buffer的序列号不会和其他东西同时存在于一个缓存行中,从而解决伪共享的问题:

	private long p1, p2, p3, p4, p5, p6, p7;
	private final long indexMask = 0;
	private long p8, p9, p10, p11, p12, p13, p14;

2.@sun.misc.Contended注解

在Java 8中,提供了@sun.misc.Contended注解来避免伪共享,原理是在使用此注解的对象或字段的前后各增加128字节大小的padding,使用2倍于大多数硬件缓存行的大小来避免相邻扇区预取导致的伪共享冲突。

    /** The current seed for a ThreadLocalRandom */
    @sun.misc.Contended("tlr")
    long threadLocalRandomSeed;

    /** Probe hash value; nonzero if threadLocalRandomSeed initialized */
    @sun.misc.Contended("tlr")
    int threadLocalRandomProbe;

    /** Secondary seed isolated from public ThreadLocalRandom sequence */
    @sun.misc.Contended("tlr")
    int threadLocalRandomSecondarySeed;

例如,Thread中有三个变量threadLocalRandomSeed,threadLocalRandomProbe,threadLocalRandomSecondarySeed就是通过注解的方法来操作缓存行的。不同的是,这里是让三个变量处于同一个缓存行,从而达到任意使一个变量改变,其他两个值也必须重新加载的目的。

6总结

1.缓存时为了平衡CPU和内存之间的速度差异,cpu核独自拥有L1、L2缓存,公用L3缓存,且越靠近CPU的缓存存储速度越快、容量越小。

2.缓存中的内容是按块一次性从主内存中加载到缓存行。

3.L1、L2缓存中的缓存行修改时,会写回到L3和主内存,并且使其他核上的还缓存行失效。

4.多个CPU核一写一读,或者同时写同一块缓存行的时候,会导致伪共享问题。

5.假如存在多个线程同时修改一个类的不同字段的场景,必须考虑使用缓存行填充或者@sun.misc.Contended注解防止伪共享。

 

参考文章:

Java8使用@sun.misc.Contended避免伪共享

剖析Disruptor:为什么会这么快?(二)神奇的缓存行填充

 

 

<think>好的,我现在需要解决用户遇到的Java编译错误:在文件Test18_volatile_2_FalseSharing.java的第62行第10列,提示找不到符号`Contended`类,位置在包`sun.misc`下。首先,我得分析可能的原因。 首先,用户可能尝试使用`@Contended`注解来避免共享。这个注解属于`sun.misc`包,但根据我的记忆,`sun`包下的类是不公开的,属于内部API,不同版本的JDK可能会有变动。比如,Java 8中这个注解确实存在,但可能被限制访问,或者在高版本中被移除或移到其他包。 接下来,我要考虑用户使用的JDK版本。如果用户用的是Java 8,可能需要检查是否添加了JVM参数`-XX:-RestrictContended`来允许使用这个注解。如果没有这个参数,编译器可能无法访问`@Contended`,导致找不到符号的错误。 如果用户使用的是Java 9及以上版本,问题更复杂。因为Java 9引入了模块化系统,默认情况下禁止访问内部API,包括`sun.misc`中的类。这时候即使用户正确引用了,编译也会失败,除非显式地通过`--add-exports`参数开放模块。 另外,用户可能在代码中导入了错误的包。例如,正确的导入应该是`import sun.misc.Contended;`,但如果包路径错误或者类名拼写错误,同样会导致问题。 还有一种可能性是用户没有使用支持`@Contended`的JDK版本。例如,某些早期版本可能没有这个注解,或者它存在于不同的包中。 解决方案方面,首先建议用户确认JDK版本。如果是Java 8,添加`-XX:-RestrictContended`参数。如果是更高版本,考虑使用替代方案,比如JDK 15引入的`@jdk.internal.vm.annotation.Contended`,但同样需要注意模块访问权限。或者,使用第三方库如Apache Commons的填充方式手动避免共享。 最后,提醒用户依赖内部API的风险,可能导致代码在不同JDK版本间不兼容,建议优先使用标准API或已知的跨版本解决方案。</think>你遇到的编译错误是因为Java代码中引用了`sun.misc.Contended`类但找不到该符号。这通常由以下几个原因及解决方案引起: --- ### **1. `@Contended`注解的作用** `@Contended`是Java内部用于解决**共享(False Sharing)**问题的注解。它通过在字段间插入填充字节(padding),确保被注解的字段独占一个缓存行,从而避免多线程环境下不同变量竞争同一缓存行导致的性能问题。 --- ### **2. 错误原因分析** - **JDK版本兼容性**: - `@Contended`属于`sun.misc`包下的内部API,**Oracle JDK 8**中可用,但需要添加JVM参数`-XX:-RestrictContended`才能访问。 - 在**JDK 9及以上版本**中,由于模块化系统的限制,`sun.misc`包默认不可访问,导致编译失败。 - **代码导入问题**: 检查代码中是否有正确的导入语句: ```java import sun.misc.Contended; // 需要确认是否存在或权限 ``` --- ### **3. 解决方案** #### **情况1:使用JDK 8** 1. **编译时添加JVM参数**: 在编译和运行时添加以下参数以解除限制: ```bash javac -XX:-RestrictContended YourClass.java java -XX:-RestrictContended YourClass ``` 2. **Maven/Gradle配置**(可选): 在构建工具的配置中设置JVM参数: ```xml <!-- Maven示例 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <argLine>-XX:-RestrictContended</argLine> </configuration> </plugin> ``` #### **情况2:使用JDK 9+** 1. **替换为标准API**: JDK 15+ 提供了公开的`@jdk.internal.vm.annotation.Contended`,但需要添加模块权限: ```java import jdk.internal.vm.annotation.Contended; ``` 编译时添加模块开放参数: ```bash javac --add-exports java.base/jdk.internal.vm.annotation=ALL-UNNAMED YourClass.java ``` 2. **手动填充替代方案**: 如果必须兼容多版本,可手动添加填充字段: ```java public class Example { volatile long value; // 手动填充(假设缓存行大小为64字节) private volatile long p1, p2, p3, p4, p5, p6, p7; // 56字节填充 } ``` --- ### **4. 验证共享修复** 可通过性能测试工具(如JMH)验证优化效果。以下是一个简单的JMH基准测试示例: ```java @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.MILLISECONDS) public class FalseSharingBenchmark { private static class ValuePadding { @Contended // 或手动填充 volatile long value; } @Benchmark public void test(Blackhole bh) { ValuePadding v = new ValuePadding(); for (int i = 0; i < 1000000; i++) { v.value = i; } bh.consume(v); } } ``` --- ### **5. 注意事项** - **避免依赖内部API**:`sun.misc`下的类不是Java标准API,未来版本可能移除或变更。 - **权衡性能可维护性**:手动填充需根据具体CPU缓存行大小调整,通常64字节为主流。 如果仍有问题,请提供完整的代码片段和JDK版本,以便进一步分析。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值