系统性能百倍提升典型案例分析:高性能队列Disruptor

}

}

/* 指定 RingBuffer 大小, 9 // 必须是 2 的 N 次方 */

int bufferSize = 1024;

/* 构建 Disruptor */

Disruptor disruptor

= new Disruptor<>(

LongEvent: : new,

bufferSize,

DaemonThreadFactory.INSTANCE );

/* 注册事件处理器 */

disruptor.handleEventsWith(

(event, sequence, endOfBatch) - >

System.out.println( "E: " + event ) );

/* 启动 Disruptor */

disruptor.start();

/* 获取 RingBuffer */

RingBuffer ringBuffer

= disruptor.getRingBuffer();

/* 生产 Event */

ByteBuffer bb = ByteBuffer.allocate( 8 );

for ( long l = 0; true; l++ )

{

bb.putLong( 0, l );

/* 生产者生产消息 */

ringBuffer.publishEvent(

(event, sequence, buffer) - >

event.set( buffer.getLong( 0 ) ), bb );

Thread.sleep( 1000 );

}

RingBuffer 如何提升性能

=================

Java SDK 中 ArrayBlockingQueue 使用数组作为底层的数据存储,而 Disruptor 是使用RingBuffer作为数据存储。RingBuffer 本质上也是数组,所以仅仅将数据存储从数组换成RingBuffer 并不能提升性能,但是 Disruptor 在 RingBuffer 的基础上还做了很多优化,其中一项优化就是和内存分配有关的。

在介绍这项优化之前,你需要先了解一下程序的局部性原理。简单来讲,程序的局部性原理指的是在一段时间内程序的执行会限定在一个局部范围内。这里的“局部性”可以从两个方面来理解,一个是时间局部性,另一个是空间局部性。时间局部性指的是程序中的某条指令一旦被执行,不久之后这条指令很可能再次被执行;如果某条数据被访问,不久之后这条数据很可能再次被访问。而空间局部性是指某块内存一旦被访问,不久之后这块内存附近的内存也很可能被访问。

CPU 的缓存就利用了程序的局部性原理:CPU 从内存中加载数据 X 时,会将数据 X 缓存在高速缓存 Cache 中,实际上 CPU 缓存 X 的同时,还缓存了 X 周围的数据,因为根据程序具备局部性原理,X 周围的数据也很有可能被访问。从另外一个角度来看,如果程序能够很好地体现出局部性原理,也就能更好地利用 CPU 的缓存,从而提升程序的性能。Disruptor 在设计 RingBuffer 的时候就充分考虑了这个问题,下面我们就对比着ArrayBlockingQueue 来分析一下。

首先是 ArrayBlockingQueue。生产者线程向 ArrayBlockingQueue 增加一个元素,每次增加元素 E 之前,都需要创建一个对象 E,如下图所示,ArrayBlockingQueue 内部有 6个元素这 6 个元素都是由生产者线程创建的,由于创建这些元素的时间基本上是离散的,所以这些元素的内存地址大概率也不是连续的。

系统性能百倍提升典型案例分析:高性能队列Disruptor

ArrayBlockingQueue 内部结构图

下面我们再看看 Disruptor 是如何处理的。Disruptor 内部的 RingBuffer 也是用数组实现的,但是这个数组中的所有元素在初始化时是一次性全部创建的,所以这些元素的内存地址大概率是连续的,相关的代码如下所示。

for ( int i = 0; i < bufferSize; i++ )

{

/*

  • entries[] 就是 RingBuffer 内部的数组

  • eventFactory 就是前面示例代码中传入的 LongEvent::new

*/

entries[BUFFER_PAD + i]

= eventFactory.newInstance();

}

Disruptor 内部 RingBuffer 的结构可以简化成下图,那问题来了,数组中所有元素内存地址连续能提升性能吗?能!为什么呢?因为消费者线程在消费的时候,是遵循空间局部性原理的,消费完第 1 个元素,很快就会消费第 2 个元素;当消费第 1 个元素 E1 的时候,CPU 会把内存中 E1 后面的数据也加载进 Cache,如果 E1 和 E2 在内存中的地址是连续的,那么 E2 也就会被加载进 Cache 中,然后当消费第 2 个元素的时候,由于 E2 已经在Cache 中了,所以就不需要从内存中加载了,这样就能大大提升性能。

系统性能百倍提升典型案例分析:高性能队列Disruptor

Disruptor 内部 RingBuffer 结构图

除此之外,在 Disruptor 中,生产者线程通过 publishEvent() 发布 Event 的时候,并不是创建一个新的 Event,而是通过 event.set() 方法修改 Event, 也就是说 RingBuffer 创建的 Event 是可以循环利用的,这样还能避免频繁创建、删除 Event 导致的频繁 GC 问题。

如何避免“伪共享”

=========

高效利用 Cache,能够大大提升性能,所以要努力构建能够高效利用 Cache 的内存结构。而从另外一个角度看,努力避免不能高效利用 Cache 的内存结构也同样重要。

有一种叫做“伪共享(False sharing)”的内存布局就会使 Cache 失效,那什么是“伪共享”呢?

伪共享和 CPU 内部的 Cache 有关,Cache 内部是按照缓存行(Cache Line)管理的,缓存行的大小通常是 64 个字节;CPU 从内存中加载数据 X,会同时加载 X 后面(64-size(X))个字节的数据。下面的示例代码出自 Java SDK 的 ArrayBlockingQueue,其内部维护了 4 个成员变量,分别是队列数组 items、出队索引 takeIndex、入队索引putIndex 以及队列中的元素总数 count。

/** 队列数组 */

final Object[] items;

/** 出队索引 */

int takeIndex;

/** 入队索引 */

int putIndex;

/** 队列中元素总数 */

int count;

当 CPU 从内存中加载 takeIndex 的时候,会同时将 putIndex 以及 count 都加载进Cache。下图是某个时刻 CPU 中 Cache 的状况,为了简化,缓存行中我们仅列出了takeIndex 和 putIndex。

系统性能百倍提升典型案例分析:高性能队列Disruptor

CPU 缓存示意图

假设线程 A 运行在 CPU-1 上,执行入队操作,入队操作会修改 putIndex,而修改putIndex 会导致其所在的所有核上的缓存行均失效;此时假设运行在 CPU-2 上的线程执行出队操作,出队操作需要读取 takeIndex,由于 takeIndex 所在的缓存行已经失效,所以 CPU-2 必须从内存中重新读取。入队操作本不会修改 takeIndex,但是由于 takeIndex和 putIndex 共享的是一个缓存行,就导致出队操作不能很好地利用 Cache,这其实就是伪共享。简单来讲,伪共享指的是由于共享缓存行导致缓存无效的场景

ArrayBlockingQueue 的入队和出队操作是用锁来保证互斥的,所以入队和出队不会同时发生。如果允许入队和出队同时发生,那就会导致线程 A 和线程 B 争用同一个缓存行,这样也会导致性能问题。所以为了更好地利用缓存,我们必须避免伪共享,那如何避免呢?

系统性能百倍提升典型案例分析:高性能队列Disruptor

CPU 缓存失效示意图

方案很简单,每个变量独占一个缓存行、不共享缓存行就可以了,具体技术是缓存行填充。比如想让 takeIndex 独占一个缓存行,可以在 takeIndex 的前后各填充 56 个字节,这样就一定能保证 takeIndex 独占一个缓存行。下面的示例代码出自 Disruptor,Sequence对象中的 value 属性就能避免伪共享,因为这个属性前后都填充了 56 个字节。Disruptor中很多对象,例如 RingBuffer、RingBuffer 内部的数组都用到了这种填充技术来避免伪共享。

/* 前:填充 56 字节 */

class LhsPadding {

long p1, p2, p3, p4, p5, p6, p7;

}

class Value extends LhsPadding {

volatile long value;

}

/* 后:填充 56 字节 */

class RhsPadding extends Value {

我的面试宝典:一线互联网大厂Java核心面试题库

以下是我个人的一些做法,希望可以给各位提供一些帮助:

整理了很长一段时间,拿来复习面试刷题非常合适,其中包括了Java基础、异常、集合、并发编程、JVM、Spring全家桶、MyBatis、Redis、数据库、中间件MQ、Dubbo、Linux、Tomcat、ZooKeeper、Netty等等,且还会持续的更新…可star一下!

image

283页的Java进阶核心pdf文档

Java部分:Java基础,集合,并发,多线程,JVM,设计模式

数据结构算法:Java算法,数据结构

开源框架部分:Spring,MyBatis,MVC,netty,tomcat

分布式部分:架构设计,Redis缓存,Zookeeper,kafka,RabbitMQ,负载均衡等

微服务部分:SpringBoot,SpringCloud,Dubbo,Docker

image

还有源码相关的阅读学习

image

加入社区:https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0
,Redis缓存,Zookeeper,kafka,RabbitMQ,负载均衡等

微服务部分:SpringBoot,SpringCloud,Dubbo,Docker

[外链图片转存中…(img-WLEb3WkD-1725614253804)]

还有源码相关的阅读学习

[外链图片转存中…(img-2WZgeb8x-1725614253805)]

加入社区:https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值