threadsafe
这篇博客文章通过说明我们如何轻松访问线程内存来进行线程安全操作,继续了我在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
threadsafe