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

简介: JEP解读与尝鲜系列2 - JEP 142 缓存行填充简化(上)
本文基于 OpenJDK 8 ~ 14 的版本


JEP 142 内容


用于将某个或者某些需要多线程读取和修改的 field 进行缓存行填充。同时由于 Java 8 之前对于缓存行填充的方式,比较繁琐且不够优雅,还有可能缓存行大小不一的问题,所以这个 JEP 中引入了 @Contended 注解。


什么是缓存行填充以及 False Sharing


CPU 缓存结构:


image.png


CPU 只能直接处理寄存器中的数据,从上面这些缓存读取,其实就是从这些缓存复制数据到寄存器。就像数据库和缓存关系相似,存在L1缓存,L2缓存,L3缓存来缓存内存中的数据。 级别越小,CPU访问越快:


image.png


上面说读取其实就是从这些缓存复制数据到寄存器,从内存读取数据也是一样,从内存复制到缓存中。但是这个复制,并不是一个字节一个字节复制的,而是一行一行复制的,这个行就是 缓存行缓存行: CPU缓存并不是将内存数据一个一个的缓存起来,而是每次从内存中取出一行内存,称为缓存行(Cache Line),对于我的电脑,缓存行长度是 64 Bytes:


image.png


对于Java,举个例子,假设 X 和 Y 两个volatile的 long 变量(Java中占用 8 Bytes),他们两个内存相邻,而且加起来的长度小于 64 Bytes,那么他们就很可能会被同时加载在同一个缓存行之中。volatile的作用就是当一个线程更新某个volatile声明的变量时,会通知其他的cpu使缓存失效,从而其他cpu想要做更新操作时,需要从内存重新读取数据。而且 Java 考虑到缓存行大小,做了 8 Bytes 对齐,所以基本不会发生一个缓存行加载正好不够 X 或者 Y 变量大小的问题。在 X,Y被加载到同一个缓存行的时候,就会发生 False Sharing 的问题,导致性能下降。


image.png


假设有两个线程分别访问并修改X和Y这两个变量,X和Y恰好在同一个缓存行上,这两个线程分别在不同的CPU上执行。那么每个CPU分别更新好X和Y时将缓存行刷入内存时,发现有别的修改了各自缓存行内的数据,这时缓存行会失效,从L3中重新获取。这样的话,程序执行效率明显下降。 为了减少这种情况的发生,其实就是避免X和Y在同一个缓存行中,可以主动添加一些无关变量将缓存行填充满,比如在X对象中添加一些变量,让它有64 Byte那么大,正好占满一个缓存行。这个操作被称为 缓存行填充


一般框架填充方式 与 需要缓存行填充的场景


可以参考的框架有很多很多,这里举两个例子,一个是高性能缓存框架 Disruptor,另一个是高性能缓存框架 Caffeine,他们都是针对缓存队列的使用,一个是环形队列,一个是普通队列。通过这两个框架了解缓存行填充的使用。


Disruptor 缓存行填充应用举例

Disruptor 结构:


image.png


每个RingBuffer是一个环状队列,队列中每个元素可以理解为一个槽。在初始化时,RingBuffer规定了总大小,就是这个环最多可以容纳多少槽。这里Disruptor规定了,RingBuffer大小必须是2的n次方。这里用了一个小技巧,就是将取模转变为取与运算。在内存管理中,我们常用的就是取余定位操作。如果我们想在Ringbuffer定位,一般会用到某个数字对Ringbuffer的大小取余。如果是对2的n次方取余,则可以简化成:

m % 2^n = m & ( 2^n - 1 )

Producer会向这个RingBuffer中填充元素,填充元素的流程是首先从RingBuffer读取下一个Sequence,之后在这个Sequence位置的槽填充数据,之后发布。 这个 Sequence 类,其中的 value 这个 field, 就是其中保存的值。这个值的修改,就涉及到了 false sharing 的问题。因为:

  1. 环形 Buffer 中的相邻的 Sequence 内存地址也是相邻的,因为底层实现就像一个数组。
  2. 如果没有缓存行填充,那么极有可能,更新当前这个 Sequence 的线程对应的缓存行,将相邻的其他 Sequence里面的值也读取了出来,导致其他生产者线程需要重新读取其他的 Sequence。这个在多生产者的场景里面比较常见

所以,需要对于这个 Sequence 里面的 value,进行缓存行填充。代码里是怎么实现的呢,从下面的类继承图,可以看出:


微信图片_20220624123529.jpg


Caffeine 应用举例

Caffeine 是一个高性能的并且拥有 Java 8 新特性的本地缓存框架。在 Spring Boot 2.0以上的版本甚至已经将 Caffeine 作为标准的缓存框架。在大多数场景下可以替换 guava cache 成为首选的本地缓存方案。同时,还提供了一个对于本地缓存任务队列,针对多个生产者,一个消费者的任务队列的工具类,这个类就是我们现在要讨论的 SingleConsumerQueue.


一个队列,涉及到并发修改,那么肯定队列头还有队列尾是需要并发修改访问的地方,至于 value,由于每次都是新生成的包装对象,所以内存并不会在一起,不予考虑。但是队列头尾是肯定内存在一起的,为了提高效率,用缓存行填充来避免队列头尾在同一个缓存行中:

final class SCQHeader {
  abstract static class PadHead<E> extends AbstractQueue<E> {
    long p00, p01, p02, p03, p04, p05, p06, p07;
    long p10, p11, p12, p13, p14, p15, p16;
  }
  /** Enforces a memory layout to avoid false sharing by padding the head node. */
  abstract static class HeadRef<E> extends PadHead<E> {
    //队列头
    @Nullable Node<E> head;
  }
  abstract static class PadHeadAndTail<E> extends HeadRef<E> {
    long p20, p21, p22, p23, p24, p25, p26, p27;
    long p30, p31, p32, p33, p34, p35, p36;
  }
  /** Enforces a memory layout to avoid false sharing by padding the tail node. */
  abstract static class HeadAndTailRef<E> extends PadHeadAndTail<E> {
    static final long TAIL_OFFSET = UnsafeAccess.objectFieldOffset(HeadAndTailRef.class, "tail");
    //队列尾
    @Nullable volatile Node<E> tail;
    void lazySetTail(Node<E> next) {
      UnsafeAccess.UNSAFE.putOrderedObject(this, TAIL_OFFSET, next);
    }
    boolean casTail(Node<E> expect, Node<E> update) {
      return UnsafeAccess.UNSAFE.compareAndSwapObject(this, TAIL_OFFSET, expect, update);
    }
  }
}
相关文章
|
5月前
|
存储 缓存 程序员
C++一分钟之-缓存行与伪共享问题
【7月更文挑战第11天】在计算机科学中,缓存是一个至关重要的概念,它能够显著提高数据访问速度。然而,缓存的使用并非没有问题,其中最著名的问题之一就是伪共享。
48 1
|
缓存 Java
JEP解读与尝鲜系列2 - JEP 142 缓存行填充简化(下)
JEP解读与尝鲜系列2 - JEP 142 缓存行填充简化(下)
JEP解读与尝鲜系列2 - JEP 142 缓存行填充简化(下)
|
缓存 Java
探讨缓存行与伪共享
最近项目中有个需求,需要用到有界队列对访问请求量进行流量削峰请求,同时作为一个缓冲层对请求处理进行后续处理,Java 内置有界队列 ArrayBlockingQueue 可以满足这方面的需求,但是性能上并不满足,于是使用了 Disruptor,它是英国外汇交易公司 LMAX 开发的一个高性能队列,了解到它内部解决伪共享问题,今天就和大家一起学习缓存行与伪共享相关的知识。
216 0
探讨缓存行与伪共享
|
缓存 JavaScript 前端开发
200 行 TypeScript 代码实现一个高效缓存库 下
200 行 TypeScript 代码实现一个高效缓存库 下
284 0
|
缓存 JavaScript 小程序
200 行 TypeScript 代码实现一个高效缓存库 上
200 行 TypeScript 代码实现一个高效缓存库 上
242 0
|
6天前
|
存储 缓存 NoSQL
解决Redis缓存数据类型丢失问题
解决Redis缓存数据类型丢失问题
125 85
|
2月前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
84 6
|
4天前
|
缓存 监控 NoSQL
Redis经典问题:缓存穿透
本文详细探讨了分布式系统和缓存应用中的经典问题——缓存穿透。缓存穿透是指用户请求的数据在缓存和数据库中都不存在,导致大量请求直接落到数据库上,可能引发数据库崩溃或性能下降。文章介绍了几种有效的解决方案,包括接口层增加校验、缓存空值、使用布隆过滤器、优化数据库查询以及加强监控报警机制。通过这些方法,可以有效缓解缓存穿透对系统的影响,提升系统的稳定性和性能。
|
1月前
|
缓存 NoSQL 关系型数据库
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
|
1月前
|
存储 缓存 NoSQL
【赵渝强老师】基于Redis的旁路缓存架构
本文介绍了引入缓存后的系统架构,通过缓存可以提升访问性能、降低网络拥堵、减轻服务负载和增强可扩展性。文中提供了相关图片和视频讲解,并讨论了数据库读写分离、分库分表等方法来减轻数据库压力。同时,文章也指出了缓存可能带来的复杂度增加、成本提高和数据一致性问题。
【赵渝强老师】基于Redis的旁路缓存架构