并发计数器几乎是每个系统的一部分,用于收集数据,线程同步等。Java对基于堆的计数器有很好的支持。
当您需要可以在处理器之间共享的计数器时,可能会有用例。
如何建立进程间计数器
数据库
这是我想到的第一个选项,数据库序列是可以被多个进程使用的计数器。 所有并发都由数据库处理。 对于初学者来说这是一个不错的选择,但我们知道您从数据库中获得的间接费用类型(网络,锁等)。 只有拉里·埃利西恩(Larry Elision)对此会感到高兴,而不是您!
一些服务器
您可以开发一些提供此类服务的服务器/中间件。 此选项仍将具有网络延迟,编组/非编组开销。
内存映射文件
您可以使用内存映射文件来执行此操作。 通过查看PeterLawrey 的Java中的线程安全进程间共享内存,我有了主意。
多进程计数器涉及的挑战。
数据可视性
一个过程完成的更改应该对所有过程可见。 通过使用内存映射文件可以解决此问题。 操作系统对此提供了保证,并且Java内存模型支持它使成为可能。
线程安全
计数器是关于多个作者的,因此线程安全成为一个大问题。 比较和交换是处理多个编写器的一种选择。 是否可以将CAS用于堆外操作? 是的,有可能这样做,欢迎来到Unsafe。 通过使用Memorymapped&Unsafe,可以将CAS用于非堆操作。
在此博客中,我将分享使用CAS映射内存的实验。
怎么样?
如何获得内存地址
MappedByteBuffer使用DirectByteBuffer,这是堆内存不足。 因此,可以获取内存的虚拟地址并使用不安全的方法来执行CAS操作。 让我们看一下代码。
FileChannel fc = new RandomAccessFile(fileName, "rw").getChannel();
// Map 8 bytes for long value.
mem = fc.map(FileChannel.MapMode.READ_WRITE, 0, 8);
startAddress = ((DirectBuffer) mem).address();
上面的代码创建8字节的内存映射文件并获取虚拟地址。 该地址可用于读取/写入内存映射文件的内容。
如何以线程安全的方式写/读
public boolean increment() {
long orignalValue = readVolatile(startAddress);
long value = convert(orignalValue);
return UnsafeUtils.unsafe.compareAndSwapLong(null,
startAddress,orignalValue, convert(value + 1));
}
public long get() {
long orignalValue = readVolatile(startAddress);
return convert(orignalValue);
}
// Only unaligned is implemented
private static long readVolatile(long position) {
if (UnsafeUtils.unaligned()) {
return UnsafeUtils.unsafe.getLongVolatile(null, position);
}
throw new UnsupportedOperationException();
}
private static long convert(long a) {
if (UnsafeUtils.unaligned()) {
return (UnsafeUtils.nativeByteOrder ? a : Long.reverseBytes(a));
}
throw new UnsupportedOperationException();
}
要查看的重要功能是readVolatile和增量 readVolatile直接从内存读取, 增量使用不安全的方法对从MemoryByteBuffer获得的地址执行CAS。
性能
我系统上的一些性能数字。 每个线程将计数器增加一百万次。
计数器的性能很好,随着线程数量的增加,CAS故障开始发生并且性能开始下降。 这些计数器的性能可以通过具有多个段来减少写争用来提高。 我将在下一个博客中写它。
结论
- 内存映射文件非常强大,可用于开发很多东西,例如堆外集合,IPC,堆外线程协调等。
- 内存映射文件为GC较少编程打开了大门。
该博客中使用的所有代码都可以在github上找到 。
翻译自: https://www.javacodegeeks.com/2014/03/off-heap-concurrent-counter.html