支撑百万并发的“零拷贝”技术,你了解吗?( 十 )

  • throw new InternalError(e);
  • }
  • return dbb;
  • }
  • private static void initDBBRConstructor() {
  • AccessController.doPrivileged(new PrivilegedAction<Void>() {
  • public Void run() {
  • try {
  • Class<?> cl = Class.forName("java.nio.DirectByteBufferR");
  • Constructor<?> ctor = cl.getDeclaredConstructor(
  • new Class<?>[] { int.class, long.class, FileDescriptor.class,
  • Runnable.class });
  • ctor.setAccessible(true);
  • directByteBufferRConstructor = ctor;
  • } catch (ClassNotFoundException | NoSuchMethodException |
  • IllegalArgumentException | ClassCastException x) {
  • throw new InternalError(x);
  • }
  • return null;
  • }});
  • }
  • DirectByteBuffer 是 MappedByteBuffer 的具体实现类 。
    实际上,Util.newMappedByteBuffer() 方法通过反射机制获取 DirectByteBuffer 的构造器,然后创建一个 DirectByteBuffer 的实例,对应的是一个单独用于内存映射的构造方法:
    1. protected DirectByteBuffer(int cap, long addr, FileDescriptor fd, Runnable unmapper) {
    2. super(-1, 0, cap, cap, fd);
    3. address = addr;
    4. cleaner = Cleaner.create(this, unmapper);
    5. att = null;
    6. }
    因此,除了允许分配操作系统的直接内存以外,DirectByteBuffer 本身也具有文件内存映射的功能,这里不做过多说明 。
    我们需要关注的是,DirectByteBuffer 在 MappedByteBuffer 的基础上提供了内存映像文件的随机读取 get() 和写入 write() 的操作 。
    内存映像文件的随机读操作:
    1. public byte get() {
    2. return ((unsafe.getByte(ix(nextGetIndex()))));
    3. }
    4. public byte get(int i) {
    5. return ((unsafe.getByte(ix(checkIndex(i)))));
    6. }
    内存映像文件的随机写操作:
    1. public ByteBuffer put(byte x) {
    2. unsafe.putByte(ix(nextPutIndex()), ((x)));
    3. return this;
    4. }
    5. public ByteBuffer put(int i, byte x) {
    6. unsafe.putByte(ix(checkIndex(i)), ((x)));
    7. return this;
    8. }
    内存映像文件的随机读写都是借助 ix() 方法实现定位的,ix() 方法通过内存映射空间的内存首地址(address)和给定偏移量 i 计算出指针地址,然后由 unsafe 类的 get() 和 put() 方法和对指针指向的数据进行读取或写入 。
    1. private long ix(int i) {
    2. return address + ((long)i << 0);
    3. }
    FileChannel
    FileChannel 是一个用于文件读写、映射和操作的通道,同时它在并发环境下是线程安全的 。
    基于 FileInputStream、FileOutputStream 或者 RandomaccessFile 的 getChannel() 方法可以创建并打开一个文件通道 。
    FileChannel 定义了 transferFrom() 和 transferTo() 两个抽象方法,它通过在通道和通道之间建立连接实现数据传输的 。
    transferTo():通过 FileChannel 把文件里面的源数据写入一个 WritableByteChannel 的目的通道 。
    1. public abstract long transferTo(long position, long count, WritableByteChannel target)
    2. throws IOException;
    transferFrom():把一个源通道 ReadableByteChannel 中的数据读取到当前 FileChannel 的文件里面 。
    1. public abstract long transferFrom(ReadableByteChannel src, long position, long count)
    2. throws IOException;
    下面给出 FileChannel 利用 transferTo() 和 transferFrom() 方法进行数据传输的使用示例:
    1. private static final String CONTENT = "Zero copy implemented by FileChannel";
    2. private static final String SOURCE_FILE = "/source.txt";
    3. private static final String TARGET_FILE = "/target.txt";
    4. private static final String CHARSET = "UTF-8";
    首先在类加载根路径下创建 source.txt 和 target.txt 两个文件,对源文件 source.txt 文件写入初始化数据 。
    1. @Before
    2. public void setup() {
    3. Path source = Paths.get(getClassPath(SOURCE_FILE));
    4. byte[] bytes = CONTENT.getBytes(Charset.forName(CHARSET));
    5. try (FileChannel fromChannel = FileChannel.open(source, StandardOpenOption.READ,
    6. StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
    7. fromChannel.write(ByteBuffer.wrap(bytes));
    8. } catch (IOException e) {
    9. e.printStackTrace();
    10. }
    11. }
    对于 transferTo() 方法而言,目的通道 toChannel 可以是任意的单向字节写通道 WritableByteChannel;而对于 transferFrom() 方法而言,源通道 fromChannel 可以是任意的单向字节读通道 ReadableByteChannel 。
    其中,FileChannel、SocketChannel 和 DatagramChannel 等通道实现了 WritableByteChannel 和 ReadableByteChannel 接口,都是同时支持读写的双向通道 。
    为了方便测试,下面给出基于 FileChannel 完成 channel-to-channel 的数据传输示例 。


    推荐阅读