JEP解读与尝鲜系列2 - JEP 142 缓存行填充简化(下)

简介: JEP解读与尝鲜系列2 - JEP 142 缓存行填充简化(下)

@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 传递这个参数,通过如下配置关闭:


微信图片_20220624123611.jpg


设置-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都是这么填充的:


微信图片_20220624123643.jpg


这样就算是最坏的情况下,比如缓存行从 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

可见,性能还是有显著差别的。


结论


  1. 多线程修改内存相邻的不同 field,可能会发生 false sharing,影响性能。
  2. 可以通过类继承,无用field方式进行缓存行填充,避免 false sharing。但是这种方式,限制了缓存行必须小于等于 64 bytes(这个可以自己实现,大部分缓存行是 64 bytes)。同时产生了很多无用代码
  3. Java 8 引入了 @Contended,简化代码,灵活缓存行配置,适应不同的缓存行大小
  4. 但是这个注解必须增加额外的 JVM 启动参数,否则不生效,所以公共框架类代码还是自己实现缓存行填充,我们项目使用的时候可以考虑使用这个注解。
相关文章
|
4月前
|
存储 缓存 程序员
C++一分钟之-缓存行与伪共享问题
【7月更文挑战第11天】在计算机科学中,缓存是一个至关重要的概念,它能够显著提高数据访问速度。然而,缓存的使用并非没有问题,其中最著名的问题之一就是伪共享。
41 1
|
缓存 Java 数据库
JEP解读与尝鲜系列2 - JEP 142 缓存行填充简化(上)
JEP解读与尝鲜系列2 - JEP 142 缓存行填充简化(上)
JEP解读与尝鲜系列2 - JEP 142 缓存行填充简化(上)
|
缓存 Java
探讨缓存行与伪共享
最近项目中有个需求,需要用到有界队列对访问请求量进行流量削峰请求,同时作为一个缓冲层对请求处理进行后续处理,Java 内置有界队列 ArrayBlockingQueue 可以满足这方面的需求,但是性能上并不满足,于是使用了 Disruptor,它是英国外汇交易公司 LMAX 开发的一个高性能队列,了解到它内部解决伪共享问题,今天就和大家一起学习缓存行与伪共享相关的知识。
210 0
探讨缓存行与伪共享
|
缓存 JavaScript 前端开发
200 行 TypeScript 代码实现一个高效缓存库 下
200 行 TypeScript 代码实现一个高效缓存库 下
276 0
|
缓存 JavaScript 小程序
200 行 TypeScript 代码实现一个高效缓存库 上
200 行 TypeScript 代码实现一个高效缓存库 上
230 0
|
1月前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(一)
数据的存储--Redis缓存存储(一)
|
1月前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(二)
数据的存储--Redis缓存存储(二)
数据的存储--Redis缓存存储(二)
|
1月前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
74 6
|
8天前
|
缓存 NoSQL 关系型数据库
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
|
10天前
|
存储 缓存 NoSQL
【赵渝强老师】基于Redis的旁路缓存架构
本文介绍了引入缓存后的系统架构,通过缓存可以提升访问性能、降低网络拥堵、减轻服务负载和增强可扩展性。文中提供了相关图片和视频讲解,并讨论了数据库读写分离、分库分表等方法来减轻数据库压力。同时,文章也指出了缓存可能带来的复杂度增加、成本提高和数据一致性问题。
【赵渝强老师】基于Redis的旁路缓存架构