10.3 NIO中的通道和缓冲区
NIO 中的缓冲区是对高性能 I/O 的一种低层抽象,为指定基本类型组成的线性序列提供容器。后面的示例都以处理 ByteBuffer 对象(最常见)为例。
10.3.1 ByteBuffer对象
ByteBuffer 对象是字节序列,理论上,在注重性能的场合中可以代替 byte[] 类型的数组。为了得到最好的性能,ByteBuffer 支持直接使用 JVM 所在平台提供的本地功能处理缓冲区。
这种方式叫作“直接缓冲区”,只要可能就会绕过 Java 堆内存。直接缓冲区在本地内存中分配,而不是在标准的 Java 堆内存中。而且,垃圾回收程序对待直接缓冲区的方式和普通的堆中 Java 对象不同。
若想创建 ByteBuffer 类型的直接缓冲区对象,可以调用工厂方法 allocateDirect()。除此之外,还有 allocate() 方法,用于创建堆中缓冲区,不过现实中不常使用。
创建字节缓冲区的第三种方式是打包现有的 byte[] 数组。这种方式创建的是堆中缓冲区,目的是以更符合面向对象的方式处理底层字节:
ByteBuffer b = ByteBuffer.allocateDirect(65536);ByteBuffer b2 = ByteBuffer.allocate(4096);byte[] data = {1, 2, 3};ByteBuffer b3 = ByteBuffer.wrap(data);
字节缓冲区只能使用低层方式访问字节,因此开发者要手动处理细节,例如需要处理字节的字节顺序和 Java 整数基本类型的符号:
b.order(ByteOrder.BIG_ENDIAN);int capacity = b.capacity();int position = b.position();int limit = b.limit();int remaining = b.remaining();boolean more = b.hasRemaining();
把数据存入缓冲区或从缓冲区中取出有两种操作方式:一种是单值操作,一次读写一个值;另一种是批量操作,一次读写一个 byte[] 数组或 ByteBuffer 对象,处理多个值(可能很多)。使用批量操作才能获得预期的性能提升:
b.put((byte)42);b.putChar('x');b.putInt(0xcafebabe);b.put(data);b.put(b2);double d = b.getDouble();b.get(data, 0, data.length);
单值形式还支持直接处理缓冲区中绝对位置上的数据:
b.put(0, (byte)9);
缓冲区这种抽象只存在于内存中,如果想影响外部世界(例如文件或网络),需要使用 Channel(通道)对象。Channel 接口在 java.nio.channels 包中定义,表示支持读写操作的实体连接。文件和套接字是两种常见的通道,不过我们要意识到,用于低延迟数据处理的自定义实现也属于通道。
通道在创建时处于打开状态,随后可以将其关闭。一旦关闭,就无法再打开。一般来说,通道要么可读要么可写,不能既可读又可写。若想理解通道,关键是要知道:
从通道中读取数据时会把字节存入缓冲区
把数据写入通道时会从缓冲区中读取字节
假如我们要计算一个大文件前 16M 数据片段的校验和:
FileInputStream fis = getSomeStream();boolean fileOK = true;try (FileChannel fchan = fis.getChannel()) {ByteBuffer buffy = ByteBuffer.allocateDirect(16 * 1024 * 1024);while(fchan.read(buffy) != -1 || buffy.position() > 0 || fileOK) {fileOK = computeChecksum(buffy);buffy.compact();}} catch (IOException e) {System.out.println("Exception in I/O");}
上述代码会尽量使用本地 I/O,不会把字节大量复制进出 Java 堆内存。如果 computeChecksum() 方法实现得好,上述代码的性能就很高。
10.3.2 映射字节缓冲区
这是一种直接字节缓冲区,包含一个内存映射文件(或内存映射文件的一部分)。这种缓冲区由 FileChannel 对象创建,不过要注意,内存映射操作之后决不能使用 MappedByteBuffer 对象对应的 File 对象,否则会抛出异常。为了规避这种问题,我们可以使用处理资源的 try 语句,严格限制相关对象的作用域:
try (RandomAccessFile raf =new RandomAccessFile(new File("input.txt"), "rw");FileChannel fc = raf.getChannel();) {MappedByteBuffer mbf =fc.map(FileChannel.MapMode.READ_WRITE, 0, fc.size());byte[] b = new byte[(int)fc.size()];mbf.get(b, 0, b.length);for (int i=0; i<fc.size(); i++) {b[i] = 0; // 这是一个副本,不会写入原文件}mbf.position(0);mbf.put(b); // 清空文件}
就算有了缓冲区,Java 在单个线程中同步执行大型 I/O 操作(例如,在文件系统之间传输 10G 数据)时还是会遇到一些限制。在 Java 7 之前,遇到这种操作时往往要自己编写多线程代码,而且要管理一个单独的线程执行后台复制操作。下面介绍 JDK 7 新添加的异步 I/O 功能。
