这篇博客文章解释了我们如何为线程安全操作轻松提供对堆内存的访问,从而继续了我在Agrona库上进行的系列文章 。 在继续进行之前,我可能应该警告一下,这是一个相当高级的主题,并且我不会尝试解释诸如内存屏障之类的概念,而只是概述API的功能。
ByteBuffer的不足
Java提供了一个字节缓冲区类来包装offheap和onheap内存。 字节缓冲区在Java网络堆栈中专门用作从中读取或写入数据的位置。
那么字节缓冲区有什么问题呢? 好吧,因为它们针对用例,所以它们不提供对原子操作之类的支持。 如果要编写从不同进程同时访问的非堆数据结构,则字节缓冲区不能满足您的需求。 您可能要编写的那种库的一个示例是一个消息队列,一个进程将读取该消息,而另一个进程将写入该消息队列。
Agrona的缓冲器
Agrona提供了几种缓冲区类和接口来克服这些缺陷。 Aeron和SBE库都使用这些缓冲区。
-
DirectBuffer
–提供从缓冲区读取值的功能的顶级接口。 -
MutableDirectBuffer
–扩展DirectBuffer
添加操作以写入缓冲区。 -
AtomicBuffer
–不,它不是核动力的MutableDirectBuffer
! 该接口添加了原子操作以及比较和交换语义。 -
UnsafeBuffer
–默认实现。 名称unsafe并不意味着不应该使用该类,只是其支持的实现使用sun.misc.Unsafe
。
拆分缓冲区而不是分配单个类的决定是出于希望限制不同系统组件对缓冲区的访问权限的考虑。 如果一个类只需要从缓冲区读取,则不应允许它通过使缓冲区发生突变而将错误引入系统。 同样,不允许设计为单线程的组件使用Atomic操作。
包装一些内存
为了能够使用缓冲区执行任何操作,您需要告诉它缓冲区的起始位置! 此过程称为包装基础内存。 所有用于包装内存的方法都称为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 length = 10;final long address = unsafe.allocateMemory(length);buffer.wrap(address, length);
包装内存还可以设置缓冲区的容量,可以通过capacity()
方法进行访问。
存取器
因此,现在您有了可以读取和写入的堆外内存缓冲区。 习惯上,每个getter都以单词get
开头,并以您要获取的值的类型作为后缀。 您需要提供一个地址,以说出要读取的缓冲区中的位置。 还有一个可选的字节顺序参数。 如果未指定字节顺序,则将使用计算机的本机顺序。 这是一个有关如何在缓冲区开始处加长的示例:
final int address = 0;long value = buffer.getLong(address, ByteOrder.BIG_ENDIAN);value++;buffer.putLong(address, value, ByteOrder.BIG_ENDIAN);
除基本类型外,还可以从缓冲区中获取和放入字节。 在这种情况下,要读取或读取的缓冲区将作为参数传递。 再次支持byte[]
, ByteBuffer
或DirectBuffer
。 例如,这是将数据读入byte[]
。
final int offsetInBuffer = 0;final int offsetInResult = 0;final int length = 5;final byte[] result = new byte[length];buffer.getBytes(offsetInBuffer, result, offsetInResult, length, result);
并发操作
int
和long
值也可以使用内存排序语义进行读取或写入。 后缀为Ordered
方法保证它们最终将被设置为所讨论的值,并且该值最终将在另一个对该值进行易失性读取的线程中可见。 换句话说, putLongOrdered
自动执行存储存储内存屏障 。 get*Volatile
和put*Volatile
遵循与Java中使用volatile关键字声明的变量的读取和写入相同的排序语义。
也可以通过AtomicBuffer
更复杂的内存操作。 例如,有一个compareAndSetLong
,它会自动在给定索引处设置一个更新的值,因为给定的现有值是一个预期值。 getAndAddLong
方法是在给定索引处添加的完全原子的方法。
生活中没有什么是免费的,所有这些都需要警告。 如果您的索引不是单词对齐的,则这些保证不存在。 请记住,它也有可能撕裂一些薄弱内存体系结构,如ARM和Sparc过字边界写入值,看到堆栈溢出对这种事情的更多细节。
边界检查
边界检查是棘手的问题之一,也是正在进行的辩论的主题。 避免边界检查可以导致更快的代码,但是会导致产生段错误并降低JVM的潜力。 Agrona的缓冲区使您可以选择通过命令行属性agrona.disable.bounds.checks
禁用边界检查,但默认情况下是边界检查。 这意味着它们的使用是安全的,但是如果应用程序对经过测试的代码进行性能分析确定边界检查是瓶颈,那么可以将其删除。
结论
Agrona的缓冲区使我们能够轻松使用offheap内存,而不受Java现有字节缓冲区强加给我们的限制。 我们正在继续扩展可从Maven Central下载的库。
感谢Mike Barker,Alex Wilson,Benji Weber,Euan Macgregor和Matthew Cranman帮助他们审阅了此博客文章。
翻译自: https://www.javacodegeeks.com/2015/08/agronas-threadsafe-offheap-buffers.html