@Contended
注解与 JVM 源码分析
通过上面的例子,我们发现,每次做缓存行填充,我们需要生成一大堆无用字段,影响代码整洁,而且未来涉及到字段重排序规则可能修改,这样会影响我们升级,所以,在 JEP 142 中,提供了 @Contended
这个注解,这个注解在 JDK 库源码中也被广泛利用,例如 ForkJoinPool 的 WorkQueue 任务队列,其中的每一个字段都是高并发访问都需要填充。但是需要注意的是,JDK 库源码中的 @Contended
注解是生效的,你应用中的默认启动参数是不生效的,需要加上:
-XX:-RestrictContended
通过这个参数关闭RestrictContended
这个状态位。
考虑到这么做不太优雅,所以 Caffeine 虽然是利用了 Java 8 的新特性,但是并没有使用这个注解。
@Contended
注解代码:
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.TYPE}) public @interface Contended { /** * The (optional) contention group tag. * This tag is only meaningful for field level annotations. * * @return contention group tag. */ String value() default ""; }
可以看到我们可以在 class 上使用这个注解,代表这个 class 的每一个 field 都需要单独的缓存行填充,防止彼此 false sharing。也可以在某个 field 上面使用,只对这个 field 进行缓存行填充。同时在 field 上面使用的时候,可以指定 group。同一个 group 下的所有 field 会被填充成尽量可以同一个缓存行读取。例如,filed1 和 field2 会被一起修改并访问,field 3 和 field 4 会被一起修改和访问,那么 field 1 和 field 2 用同一个 group, field 3 和 field 4 用同一个group, 对于 field 1 和 field 2 的读取修改,与对于 field 3 和 field 4 的读取修改,之间不会有 false sharing
对应的 JDK 源码 src/hotspot/share/classfile/classFileParser.cpp
:
AnnotationCollector::ID AnnotationCollector::annotation_index(const ClassLoaderData* loader_data, const Symbol* name) { const vmSymbols::SID sid = vmSymbols::find_sid(name); // 对于系统类加载器还有系统匿名类,设置 privileged 为 true const bool privileged = loader_data->is_the_null_class_loader_data() || loader_data->is_platform_class_loader_data() || loader_data->is_anonymous(); //... switch (sid) { //... case vmSymbols::VM_SYMBOL_ENUM_NAME(jdk_internal_vm_annotation_Contended_signature): { if (_location != _in_field && _location != _in_class) { break; } //如果没有启用Contended,或者 RestrictContended 这个 JVM Boolean Flag (就是对应上面提到的启动参数)为 Ture 并且不是 JDK 内部类,不处理 @Contended 字段 if (!EnableContended || (RestrictContended && !privileged)) { break; // honor privileges } return _jdk_internal_vm_annotation_Contended; //... } } } static void parse_annotations(const ConstantPool* const cp, const u1* buffer, int limit, AnnotationCollector* coll, ClassLoaderData* loader_data, TRAPS) { // 指定 Contended Group if (AnnotationCollector::_jdk_internal_vm_annotation_Contended == id) { //默认 group index 为0 u2 group_index = 0; // default contended group if (count == 1 && s_size == (index - index0) // match size && s_tag_val == *(abase + tag_off) && member == vmSymbols::value_name()) { group_index = Bytes::get_Java_u2((address)abase + s_con_off); if (cp->symbol_at(group_index)->utf8_length() == 0) { group_index = 0; // default contended group } } coll->set_contended_group(group_index); } }
通过编译后的结构,对比@Contended
与变量填充
定义一个类:
@Data @AllArgsConstructor public class VolatileLong { @Contended("group1") private volatile long value1; @Contended("group2") private volatile long value2; @Contended("group3") private volatile long value3; @Contended("group4") private volatile long value4; }
这里需要注意,对于 Java 9 以上的版本,编译和运行时需要增加参数:
--add-exports=java.base/jdk.internal.vm.annotation=ALL-UNNAMED
将 @Contended
注解所在的模块暴露出来,同时如果你是用 IDEA,由于IDEA 编译的时候会自动在 javac 后面添加 --release
这个参数,这个参数不允许 --add-export
等所有打破 Java 模块化的参数,所以需要关闭 IDEA 传递这个参数,通过如下配置关闭:
设置-XX:+RestrictContended
(这其实就是默认的,加不加都行),忽略Contended查看编译后对象内存结构:
OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 (alignment/padding gap) 16 8 long VolatileLong.value1 N/A 24 8 long VolatileLong.value2 N/A 32 8 long VolatileLong.value3 N/A 40 8 long VolatileLong.value4 N/A Instance size: 48 bytes Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
设置-XX:-RestrictContended
,不忽略Contended查看编译后对象内存结构:
OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 (alignment/padding gap) 16 120 long VolatileLong.value1(contended, group = group1) N/A 136 120 long VolatileLong.value2(contended, group = group2) N/A 256 120 long VolatileLong.value3(contended, group = group3) N/A 376 120 long VolatileLong.value4(contended, group = group4) N/A Instance size: 596 bytes Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
其中的每个value field都是这么填充的:
这样就算是最坏的情况下,比如缓存行从 p1 开始,或者缓存行从某一个 value 开始,如上图,都不会载入其他的value。
对比测试@Contended
性能
private static final int ITER = 1000000000; public static void main(String[] args) throws RunnerException, InterruptedException { VolatileLong volatileLong = new VolatileLong(0, 0, 0, 0); Runnable update1 = () -> { for (int i = 0; i < ITER; i++) { volatileLong.setValue1(i); } }; Runnable update2 = () -> { for (int i = 0; i < ITER; i++) { volatileLong.setValue2(i); } }; Runnable update3 = () -> { for (int i = 0; i < ITER; i++) { volatileLong.setValue3(i); } }; Runnable update4 = () -> { for (int i = 0; i < ITER; i++) { volatileLong.setValue4(i); } }; Thread[] threads = new Thread[10]; long start = System.currentTimeMillis(); for (int i = 0; i < threads.length; i++) { threads[i] = (i & 2) == 0 ? new Thread(update1) : new Thread(update2); threads[i].start(); } for (int i = 0; i < threads.length; i++) { threads[i].join(); } System.out.println("1 elapsed: " + (System.currentTimeMillis() - start)); start = System.currentTimeMillis(); for (int i = 0; i < threads.length; i++) { threads[i] = (i & 2) == 0 ? new Thread(update3) : new Thread(update4); threads[i].start(); } for (int i = 0; i < threads.length; i++) { threads[i].join(); } System.out.println("2 elapsed: " + (System.currentTimeMillis() - start)); start = System.currentTimeMillis(); for (int i = 0; i < threads.length; i++) { threads[i] = (i & 2) == 0 ? new Thread(update1) : new Thread(update3); threads[i].start(); } for (int i = 0; i < threads.length; i++) { threads[i].join(); } System.out.println("3 elapsed: " + (System.currentTimeMillis() - start)); start = System.currentTimeMillis(); for (int i = 0; i < threads.length; i++) { threads[i] = (i & 2) == 0 ? new Thread(update2) : new Thread(update4); threads[i].start(); } for (int i = 0; i < threads.length; i++) { threads[i].join(); } System.out.println("4 elapsed: " + (System.currentTimeMillis() - start)); }
对于开启 Contended 结果:
1 elapsed: 13140 2 elapsed: 11185 3 elapsed: 10124 4 elapsed: 11749
对于未开启 Contended, 出现 false sharing 的结果:
1 elapsed: 19021 2 elapsed: 18758 3 elapsed: 19140 4 elapsed: 19287
可见,性能还是有显著差别的。
结论
- 多线程修改内存相邻的不同 field,可能会发生 false sharing,影响性能。
- 可以通过类继承,无用field方式进行缓存行填充,避免 false sharing。但是这种方式,限制了缓存行必须小于等于 64 bytes(这个可以自己实现,大部分缓存行是 64 bytes)。同时产生了很多无用代码
- Java 8 引入了
@Contended
,简化代码,灵活缓存行配置,适应不同的缓存行大小 - 但是这个注解必须增加额外的 JVM 启动参数,否则不生效,所以公共框架类代码还是自己实现缓存行填充,我们项目使用的时候可以考虑使用这个注解。