Agrona 的线程安全堆外缓冲区

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

这篇博文通过解释我们如何为线程安全操作提供对堆外内存的轻松访问来继续我正在进行的关于 Agrona 库的系列文章 。在我们继续之前,我可能应该提醒一下,这是一个相当高级的主题,我不会尝试解释诸如内存屏障之类的概念 - 只是概述 API 的功能。

ByteBuffer的不足

Java 提供了一个 字节缓冲区 类来包装堆外和堆内内存。字节缓冲区专门用于 Java 网络堆栈,作为读取或写入数据的地方。

那么字节缓冲区有什么问题呢?好吧,因为他们针对的是他们的用例,所以他们不提供对原子操作之类的支持。如果您想编写一个从不同进程并发访问的堆外数据结构,那么字节缓冲区不能满足您的需求。您可能想要编写的库类型的一个示例是消息队列,一个进程将从中读取另一个进程将写入的消息队列。

阿格罗纳缓冲液

Agrona 提供了几个缓冲类和接口以克服这些缺陷。这些缓冲区由 Aeron SBE 库使用。

  1. DirectBuffer - 提供从缓冲区读取值的能力的顶级接口。
  2. MutableDirectBuffer - 扩展 DirectBuffer 添加操作以写入缓冲区。
  3. AtomicBuffer - 不,它不是核动力的 MutableDirectBuffer !该接口添加了原子操作和比较和交换语义。
  4. UnsafeBuffer - 默认实现。名称 unsafe 不应该暗示不应该使用该类,只是它的支持实现使用 sun.misc.Unsafe

拆分缓冲区而不是单一类的决定是出于希望限制不同系统组件对缓冲区的访问。如果一个类只需要从一个缓冲区中读取,那么就不应该允许它通过改变缓冲区来将错误引入系统。同样,不应允许设计为单线程的组件使用原子操作。

包装一些内存

为了能够对缓冲区做任何事情,您需要告诉它缓冲区从哪里开始!此过程称为包装底层内存。所有包装内存的方法都称为 wrap 并且可以包装 byte[] ByteBuffer DirectBuffer 。您还可以指定用于包装数据结构的偏移量和长度。例如,这是包装 byte[] 的方式。


 final int offset = 0;
        final int length = 5;
        buffer.wrap(new byte[length], offset, length);

还有一个包装选项 - 这是一个内存位置的地址。在这种情况下,该方法采用内存的基地址及其长度。这是为了支持诸如通过 sun.misc.Unsafe 或例如 malloc 调用分配的内存。这是一个使用 Unsafe 例子。


 final int offset = 0;
        final int length = 5;
        buffer.wrap(new byte[length], offset, length);

包装内存还设置了缓冲区的容量,可以通过 capacity() 方法访问。

配件

所以现在您已经有了可以读取和写入的堆外内存缓冲区。约定是每个 getter 都以单词 get 开头,并以您要获取的值的类型作为后缀。您需要提供一个地址来说明缓冲区中的读取位置。还有一个可选的字节顺序参数。如果未指定字节顺序,则将使用机器的本机顺序。这是一个如何在缓冲区的开头增加 long 的示例:


 final int offset = 0;
        final int length = 5;
        buffer.wrap(new byte[length], offset, length);

除了原始类型之外,还可以从缓冲区中获取和放入字节。在这种情况下,要读入或读出的缓冲区作为参数传递。再次支持 byte[] ByteBuffer DirectBuffer 。例如,这是将数据读入 byte[] 的方式。


 final int offset = 0;
        final int length = 5;
        buffer.wrap(new byte[length], offset, length);

并发操作

也可以使用内存排序语义读取或写入 int long 值。以 Ordered 为后缀的方法保证它们最终将被设置为有问题的值,并且该值最终将从另一个对该值进行易失性读取的线程可见。换句话说, putLongOrdered 自动执行存储存储 内存屏障 get*Volatile put*Volatile 遵循与在 Java 中使用 volatile 关键字声明的变量读写相同的顺序语义。

通过 AtomicBuffer 也可以进行更复杂的内存操作。例如,有一个 compareAndSetLong 将在给定索引处原子地设置更新值,给定现有值有一个预期值。 getAndAddLong 方法是一种在给定索引处添加的完全原子的方法。

生活中没有什么是免费的,所有这一切都有一个警告。如果您的索引不是字对齐的,这些保证是不存在的。请记住,在某些弱内存架构(例如 ARM 和 Sparc)上,也可能会 撕裂 写入超过字边界的值,有关此类事情的更多详细信息,请参阅 堆栈溢出

边界检查

边界检查是那些棘手的问题和持续争论的话题之一。避免边界检查可以提高代码速度,但可能会导致段错误并导致 JVM 崩溃。 Agrona 的缓冲区让您可以选择通过命令行属性 agrona.disable.bounds.checks 禁用边界检查,但默认情况下是边界检查。这意味着它们的使用是安全的,但是如果测试代码的应用程序分析确定边界检查是一个瓶颈,那么它可以被删除。

结论

Agrona 的缓冲区允许我们轻松使用堆外内存,而不受 Java 现有字节缓冲区强加给我们的限制。我们正在继续扩展可以从 maven central 下载的库。

感谢 Mike Barker、Alex Wilson、Benji Weber、Euan Macgregor 和 Matthew Cranman 帮助审阅这篇博文。