堆外并发计数器

并发计数器几乎每个系统都有,它通常用来收集数据、实现线程同步。Java对于基于堆的计数器有很好的支持。

在某些情况下,需要建立多个进程共享的计数器。

如何建立进程间的计数器?

数据库

数据库是我们最容易想到的方法,数据库索引是一个可以被多进程共享的计数器。所有的并发都由数据库处理。对于初学者来说这是一个不错的选择,但是我们知道数据库会降低网络、锁等一些方面的性能。只有Larry Elision(译者注:Oracle的CEO)会对此感到高兴,你不会的。

服务器

你可以开发服务器或中间件提供这类服务,不过这种方法仍然会存在网络、序列化/反序列化方面的开销。

内存映射文件

你可以通过使用内存映射文件解决这一问题。我在听了PeterLawrey关于“Java中共享内存的进程间的线程安全”演讲之后想到了这个方法。

多进程计数器中遇到的困难

数据可视化

进程对数据的改变对于其他进程是可见的。这个问题可以通过使用内存映射文件解决,操作系统可以保证这一功能的实现,Java的内存模式也能支持这种方式。

线程安全

由于计数器有多个写入者,这使得线程安全成了一个很大的问题。比较与交换(Compare-and-swap,CAS)可以用来解决多个写入者的问题。但是,是否可以使用CAS实现堆外操作呢?答案是肯定的,欢迎使用Unsafe类。通过使用内存映射与Unsafe类结合可以使用CAS实现堆外操作。

在这篇博客中我将向大家分享我使用CAS实现内存映射的经验。

到底如何实现的?

怎样或得内存地址?

MappedByteBuffer使用DirectByteBuffer,其实是堆外内存。所以,我们可以获得内存的虚拟地址,并使用Unsafe类实现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();

上面的代码创建了八字节的内存映射文件,同时获得了它的虚拟地址。这个地址可以用来读写内存映射文件中的内容。

如何实现线程安全的读写?

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和increment函数,readVolatile直接从内存读取内容,increment 使用Unsafe类在MemoryByteBuffer得到的地址上实现CAS操作。

性能

下面是系统的性能参数。每个线程计数器统计一百万次。

计数器的性能在下降,随着线程数量的增加CAS操作的失败次数也开始增加,性能开始下降。这些计数器的性能可以通过资源分割减少资源来提高。我会在下一篇博文中详细介绍。

结论

  • 内存映射文件十分有效,它可以用来开发一系列东西,如堆外集合、IPC、堆外线程同步等;
  • 内存映射文件减少了垃圾回收GC的编程。
    博客中的所有代码可以在github中下载。
原文链接: javacodegeeks 翻译: ImportNew.com - 人晓
译文链接: http://www.importnew.com/10905.html
[ 转载请保留原文出处、译者和译文链接。]

关于作者: 人晓

(新浪微博:@人晓

查看人晓的更多文章 >>



可能感兴趣的文章

发表评论

Comment form

(*) 表示必填项

1 条评论

  1. PP 说道:

    但是不同物理机上的进城之间就无力了

    Thumb up 0 Thumb down 0

跳到底部
返回顶部