053、NIO

Par @Martin dans le
Tags :

Reference:

概述

原有的 I/O 流有着太多的软件层次需要提速:

  • 小数据块的大量复制
  • 在不切换线程上下文的情况下没有办法在多个源之间进行数据的复用
  • 无法利用现代 OS 的高性能 I/O 特性 (比如内存和 file 映射)

NIO 主要变化:

  • Buffer 类允许在 JVM 和 OS 之间使用很小的内存到内存复制技术来移动数据, 而且不需要过度开销
  • 统一的 Channel 类族允许在 Buffer 和文件或 Socket 之间直接交换数据而不需要像原有的 Stream 类那样借助于中间媒介
  • NIO 首次提供了文件锁定机制

和 NIO 相关的类都在 java.nio 包下

Buffer

Heap Buffer 与 Direct Buffer.

Buffer 有两种一种是 Heap Buffer, 另一种是 Direct Buffer.

第一种方式是分配 JVM 堆内存, 属于 GC 管辖范围, 由于需要拷贝所以速度相对较慢 (由于 Java 程序运行在 JVM 中, 所以旧的 I/O 流都是需要在 OS 本地内存中转一下);
第二种方式是分配 OS 本地内存, 不属于 GC 管辖范围, 由于不需要内存拷贝所以速度相对较快.

由于构造和析构 Direct Buffer 时间成本高, 建议使用缓冲池, 参见 netty 的实现.

References:
JAVA NIO 之 Direct Buffer 与 Heap Buffer 的区别?
JAVA NIO 内存泄露

ByteBuffer

创建 ByteBuffer 有两种方法:

  • 使用 allocate() 或者 allocateDirect() 静态方法
  • 通过 wrap() 静态方法包装一个已有的数组来创建

通过 put 系列方法可将数据写入 ByteBuffer, 如:

  • putChar(char value)
  • putChar(int index, char value)
  • putInt(int value)
  • putInt(int index, int value)

然后通过 get 系列方法可获取 ByteBuffer 中的数据 (注意, 写入/读取数据顺序要一致, 才能读到想要的数据).

ByteBuffer还可以设置 byte 的顺序.

  • java.io 中数据对流的渲染总是大位在前.
  • java.nio 中则可以用指定字节顺序.
    • order(ByteOrder.BIG_ENDIAN) 大位在前
    • order(ByteOrder.LITTLE_ENDIAN) 小位在前
    • order(ByteOrder.nativeOrder()) 本地顺序

ByteBuffer 无法用于数组传输, 一般的做法是将 ByteBuffer 转换成对应类型的 Buffer, 然后向其中写入数据, 例如在当前位置向 ByteBuffer 中写入 float[]:

float[] array;
// ... 其它代码

FloarBuffer floatBuf = byteBuffer.asFloatBuffer();
floatBuf.put(array);


Channel

回想一下之前学的 I/O, 先建立一个流, 然后从流中读取/写入数据, 而在 NIO 中, 我们不用流而用 Channel, 它类似流, 但又有些不同:

  • 既可以从通道中读取数据, 又可以写数据到通道, 但流的读写通常是单向的
  • 通道中的数据总是要先读到一个 Buffer, 或者总是要从一个 Buffer 中写入
  • 通道可以异步地读写

java.nio.channels.Channel

它的 JavaDoc 如下:

A nexus for I/O operations.

A channel represents an open connection to an entity such as a hardware device, a file, a network socket, or a program component that is capable of performing one or more distinct I/O operations, for example reading or writing.

A channel is either open or closed. A channel is open upon creation, and once closed it remains closed. Once a channel is closed, any attempt to invoke an I/O operation upon it will cause a ClosedChannelException to be thrown. Whether or not a channel is open may be tested by invoking its isOpen method.

Channels are, in general, intended to be safe for multithreaded access as described in the specifications of the interfaces and classes that extend and implement this interface.

Channel 是接口, 它有一些重要的实现:

  • FileChannel 从文件中读写数据, 可从以下类包装而来
    • RandomAccessFile
    • FileInputStream
    • FileOutputStream
  • DatagramChannel 能通过 UDP 读写网络中的数据
    • 可从 DatagramSocket 包装而来
  • SocketChannel 能通过 TCP 读写网络中的数据
    • 可从 Socket 包装而来
  • ServerSocketChannel 可以监听新进来的 TCP 连接, 像 Web 服务器那样, 对每一个新进来的连接都会创建一个 SocketChannel
    • 可从 ServerSocket 包装而来

下面是一个使用 FileChannel 读取数据到 Buffer 中的示例:

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();

ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buf);
while (bytesRead != -1) {
    System.out.println("Read " + bytesRead);
    buf.flip();

    while(buf.hasRemaining()){
        System.out.print((char)buf.get());
    }

    buf.clear();
    bytesRead = inChannel.read(buf);
}
aFile.close();


注意 buf.flip() 的调用, 它的作用有两个:

  • 把 limit 设置为当前的 position 值
  • 把 position 设置为 0

然后处理的数据就是从 position 到 limit 直接的数据, 也就是刚刚读取过来的数据.

Memory Mapped

虚拟内存映射: 把文件的某一段(磁盘/硬盘上)直接映射到内存中去; 一旦文件被映射, 访问文件就会非常快, 不用通过系统的 read()write().

MappedByteBuffer FileChannel.map(MapMode, long position, longsize)
// MapMode 映射 mode: READ_ONLY、READ_WRITE、PRIVATE


通过 FileChannel 可以将文件映射到内存中, 取得一个 MappedByteBuffer.

说明:

剩下的还有 162 ~ 166 没看, 看不下去了, 以后再看吧