3303 字
17 分钟

Java NIO详解

2026-02-06
浏览量 加载中...

Java NIO详解#

什么是NIO?#

NIO(New I/O)是Java 4引入的一个新的I/O API,它提供了一种非阻塞的I/O操作方式,相比传统的IO(也称为OIO,Old I/O),NIO具有更高的性能和可扩展性。

NIO与传统IO的区别#

特性传统IONIO
操作方式阻塞式非阻塞式
数据传输单位字节流/字符流缓冲区
处理模式面向流面向通道
并发处理多线程单线程处理多个连接
性能
可扩展性

NIO的核心组件#

1. 缓冲区(Buffer)#

缓冲区是NIO的核心概念,它是一个连续的内存块,用于存储数据。所有的NIO操作都围绕缓冲区进行。

1.1 缓冲区的类型#

NIO提供了以下几种缓冲区类型:

  • ByteBuffer:存储字节数据
  • CharBuffer:存储字符数据
  • ShortBuffer:存储短整型数据
  • IntBuffer:存储整型数据
  • LongBuffer:存储长整型数据
  • FloatBuffer:存储浮点型数据
  • DoubleBuffer:存储双精度浮点型数据

1.2 缓冲区的核心属性#

每个缓冲区都有以下四个核心属性:

  • capacity:缓冲区的容量,一旦创建就不能修改
  • position:当前位置,下一个要读取或写入的位置
  • limit:限制位置,不能读取或写入超过此位置的数据
  • mark:标记位置,用于临时标记一个位置,以便后续可以通过reset()方法回到该位置

1.3 缓冲区的基本操作#

// 创建ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(1024); // 分配1024字节的缓冲区
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024); // 分配直接缓冲区
// 写入数据
buffer.put((byte) 1);
buffer.put(new byte[]{2, 3, 4});
// 切换到读模式
buffer.flip();
// 读取数据
byte b1 = buffer.get();
byte[] bytes = new byte[3];
buffer.get(bytes);
// 清空缓冲区(切换到写模式)
buffer.clear();
// 压缩缓冲区(保留未读取的数据,切换到写模式)
buffer.compact();
// 标记和重置
buffer.mark();
// 读取数据...
buffer.reset(); // 回到标记位置
// 翻转(切换到读模式)
buffer.flip();
// 重绕(保持读模式,将position重置为0)
buffer.rewind();
// 剩余元素数量
int remaining = buffer.remaining();
// 是否有剩余元素
boolean hasRemaining = buffer.hasRemaining();

2. 通道(Channel)#

通道是NIO中用于数据传输的对象,它可以从缓冲区读取数据或将数据写入缓冲区。通道是双向的,既可以读也可以写,而传统IO的流是单向的。

2.1 通道的类型#

NIO提供了以下几种通道类型:

  • FileChannel:文件通道,用于文件I/O操作
  • SocketChannel:套接字通道,用于TCP网络I/O操作
  • ServerSocketChannel:服务器套接字通道,用于监听TCP连接
  • DatagramChannel:数据报通道,用于UDP网络I/O操作
  • Pipe.SinkChannel:管道的写入端通道
  • Pipe.SourceChannel:管道的读取端通道

2.2 通道的基本操作#

// FileChannel示例
RandomAccessFile file = new RandomAccessFile("data.txt", "rw");
FileChannel fileChannel = file.getChannel();
// 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 从通道读取数据到缓冲区
int bytesRead = fileChannel.read(buffer);
while (bytesRead != -1) {
buffer.flip(); // 切换到读模式
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
buffer.clear(); // 清空缓冲区,切换到写模式
bytesRead = fileChannel.read(buffer);
}
// 写入数据到通道
buffer.put("Hello, NIO!".getBytes());
buffer.flip(); // 切换到读模式
while (buffer.hasRemaining()) {
fileChannel.write(buffer);
}
// 关闭通道和文件
fileChannel.close();
file.close();

3. 选择器(Selector)#

选择器是NIO的核心组件,它允许单个线程处理多个通道。选择器通过轮询的方式,检查多个通道是否有就绪的I/O事件。

3.1 选择器的基本操作#

// 创建选择器
Selector selector = Selector.open();
// 打开ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false); // 设置为非阻塞模式
// 注册通道到选择器
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 循环处理事件
while (true) {
// 阻塞直到有事件发生
int readyChannels = selector.select();
if (readyChannels == 0) {
continue;
}
// 获取所有就绪的SelectionKey
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isAcceptable()) {
// 处理连接事件
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 处理读取事件
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buffer);
if (bytesRead == -1) {
// 连接关闭
socketChannel.close();
key.cancel();
} else if (bytesRead > 0) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
System.out.println("Received: " + new String(data));
// 响应客户端
ByteBuffer responseBuffer = ByteBuffer.wrap("Hello from server".getBytes());
socketChannel.write(responseBuffer);
}
} else if (key.isWritable()) {
// 处理写入事件
// ...
}
// 移除已处理的SelectionKey
iterator.remove();
}
}

3.2 SelectionKey#

SelectionKey是通道和选择器之间的关联对象,它包含以下信息:

  • 通道:与选择器关联的通道
  • 选择器:与通道关联的选择器
  • 兴趣集:感兴趣的事件集合
  • 就绪集:已就绪的事件集合
  • 附件:可以附加一个对象到SelectionKey

SelectionKey的事件类型:

  • OP_ACCEPT:接受连接事件
  • OP_CONNECT:连接完成事件
  • OP_READ:可读事件
  • OP_WRITE:可写事件

4. 管道(Pipe)#

管道是NIO中用于在同一JVM内两个线程之间通信的机制。管道有两个通道:SinkChannel(写入端)和SourceChannel(读取端)。

4.1 管道的基本操作#

// 创建管道
Pipe pipe = Pipe.open();
// 获取写入端通道
Pipe.SinkChannel sinkChannel = pipe.sink();
// 获取读取端通道
Pipe.SourceChannel sourceChannel = pipe.source();
// 写入数据到管道
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Hello, Pipe!".getBytes());
buffer.flip();
sinkChannel.write(buffer);
// 从管道读取数据
buffer.clear();
int bytesRead = sourceChannel.read(buffer);
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
System.out.println("Received from pipe: " + new String(data));
// 关闭通道
sinkChannel.close();
sourceChannel.close();

NIO的高级特性#

1. 非阻塞IO#

非阻塞IO是NIO的核心特性,它允许线程在等待IO操作完成时执行其他任务,而不是被阻塞。

// 设置通道为非阻塞模式
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
// 尝试连接
boolean connected = socketChannel.connect(new InetSocketAddress("localhost", 8080));
if (!connected) {
while (!socketChannel.finishConnect()) {
// 连接尚未完成,可以执行其他任务
System.out.println("Waiting for connection...");
}
}
// 非阻塞读取
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buffer);
if (bytesRead == -1) {
// 连接关闭
} else if (bytesRead > 0) {
// 有数据可读
buffer.flip();
// 处理数据...
} else {
// 没有数据可读,可以执行其他任务
}

2. 直接缓冲区#

直接缓冲区是在堆外内存中分配的缓冲区,它可以提高I/O性能,因为它避免了数据在堆内存和本地内存之间的复制。

// 分配直接缓冲区
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);
// 使用直接缓冲区
FileChannel fileChannel = new RandomAccessFile("data.txt", "rw").getChannel();
directBuffer.put("Hello, Direct Buffer!".getBytes());
directBuffer.flip();
fileChannel.write(directBuffer);
// 关闭通道
fileChannel.close();

3. 内存映射文件#

内存映射文件是一种将文件映射到内存的技术,它可以提高文件I/O性能,因为它允许直接在内存中操作文件数据。

// 打开文件通道
FileChannel fileChannel = new RandomAccessFile("data.txt", "rw").getChannel();
// 映射文件到内存
MappedByteBuffer mappedBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileChannel.size());
// 直接操作内存中的文件数据
for (int i = 0; i < mappedBuffer.limit(); i++) {
byte b = mappedBuffer.get(i);
mappedBuffer.put(i, (byte) (b + 1)); // 简单修改数据
}
// 关闭通道
fileChannel.close();

4. 分散/聚集IO#

分散/聚集IO是一种将数据分散到多个缓冲区或从多个缓冲区聚集数据的技术,它可以提高I/O性能。

4.1 分散读取(Scattering Read)#

// 创建多个缓冲区
ByteBuffer headerBuffer = ByteBuffer.allocate(100);
ByteBuffer bodyBuffer = ByteBuffer.allocate(1024);
ByteBuffer[] buffers = {headerBuffer, bodyBuffer};
// 从通道读取数据到多个缓冲区
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 8080));
socketChannel.read(buffers);
// 处理数据
headerBuffer.flip();
bodyBuffer.flip();
// 处理headerBuffer和bodyBuffer...

4.2 聚集写入(Gathering Write)#

// 创建多个缓冲区
ByteBuffer headerBuffer = ByteBuffer.wrap("HTTP/1.1 200 OK\r\n".getBytes());
ByteBuffer bodyBuffer = ByteBuffer.wrap("Hello, World!\r\n".getBytes());
ByteBuffer[] buffers = {headerBuffer, bodyBuffer};
// 从多个缓冲区写入数据到通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 8080));
socketChannel.write(buffers);

NIO的应用场景#

1. 网络服务器#

NIO非常适合构建高性能的网络服务器,因为它可以使用单个线程处理多个连接。

public class NIOServer {
public static void main(String[] args) throws IOException {
// 创建选择器
Selector selector = Selector.open();
// 打开ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);
// 注册到选择器
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server started on port 8080");
while (true) {
// 阻塞直到有事件发生
int readyChannels = selector.select();
if (readyChannels == 0) {
continue;
}
// 处理事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isAcceptable()) {
// 处理连接事件
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("Client connected: " + socketChannel.getRemoteAddress());
} else if (key.isReadable()) {
// 处理读取事件
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buffer);
if (bytesRead == -1) {
// 连接关闭
socketChannel.close();
key.cancel();
System.out.println("Client disconnected");
} else if (bytesRead > 0) {
// 处理数据
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String message = new String(data);
System.out.println("Received: " + message);
// 响应客户端
ByteBuffer responseBuffer = ByteBuffer.wrap(("Server response: " + message).getBytes());
socketChannel.write(responseBuffer);
}
}
// 移除已处理的SelectionKey
iterator.remove();
}
}
}
}

2. 网络客户端#

public class NIOClient {
public static void main(String[] args) throws IOException {
// 打开SocketChannel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
// 连接服务器
boolean connected = socketChannel.connect(new InetSocketAddress("localhost", 8080));
if (!connected) {
while (!socketChannel.finishConnect()) {
System.out.println("Connecting to server...");
}
}
System.out.println("Connected to server");
// 发送数据
String message = "Hello, Server!";
ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
socketChannel.write(buffer);
// 读取响应
buffer.clear();
int bytesRead = socketChannel.read(buffer);
if (bytesRead > 0) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
System.out.println("Received: " + new String(data));
}
// 关闭通道
socketChannel.close();
}
}

3. 文件操作#

public class NIOFileOperations {
public static void main(String[] args) throws IOException {
// 文件复制
copyFile("source.txt", "destination.txt");
// 文件读取
readFile("source.txt");
// 文件写入
writeFile("output.txt", "Hello, NIO File Operations!");
}
private static void copyFile(String sourcePath, String destinationPath) throws IOException {
try (FileChannel sourceChannel = new FileInputStream(sourcePath).getChannel();
FileChannel destinationChannel = new FileOutputStream(destinationPath).getChannel()) {
// 使用transferTo方法复制文件,效率更高
sourceChannel.transferTo(0, sourceChannel.size(), destinationChannel);
System.out.println("File copied successfully");
}
}
private static void readFile(String filePath) throws IOException {
try (FileChannel fileChannel = new FileInputStream(filePath).getChannel()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (fileChannel.read(buffer) != -1) {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
buffer.clear();
}
System.out.println();
}
}
private static void writeFile(String filePath, String content) throws IOException {
try (FileChannel fileChannel = new FileOutputStream(filePath).getChannel()) {
ByteBuffer buffer = ByteBuffer.wrap(content.getBytes());
fileChannel.write(buffer);
System.out.println("File written successfully");
}
}
}

NIO的最佳实践#

1. 合理使用缓冲区大小#

缓冲区大小应该根据实际需求来设置,过小的缓冲区会导致频繁的I/O操作,过大的缓冲区会浪费内存。

2. 优先使用直接缓冲区#

对于大文件或频繁的I/O操作,应该使用直接缓冲区,以提高性能。

3. 正确管理缓冲区的状态#

在使用缓冲区时,应该正确管理其状态,特别是positionlimitcapacity之间的关系。

4. 合理使用选择器#

选择器是NIO的核心组件,应该合理使用它来管理多个通道,以提高并发处理能力。

5. 避免过度使用非阻塞IO#

非阻塞IO虽然性能高,但也增加了编程复杂度。对于简单的I/O操作,传统IO可能更合适。

6. 正确关闭通道和选择器#

通道和选择器是系统资源,应该使用try-with-resources语句或在finally块中关闭它们,以避免资源泄漏。

7. 考虑使用NIO.2(AIO)#

对于需要异步I/O操作的场景,应该考虑使用NIO.2(AIO),它提供了更高级的异步I/O功能。

常见陷阱#

1. 缓冲区状态管理不当#

在使用缓冲区时,忘记调用flip()clear()compact()等方法,导致缓冲区状态不正确,从而引发错误。

2. 通道关闭不当#

忘记关闭通道,导致资源泄漏。

3. 选择器使用不当#

  • 忘记移除已处理的SelectionKey,导致重复处理
  • 没有正确处理SelectionKey的各种事件类型

4. 非阻塞IO的复杂性#

非阻塞IO的编程复杂度较高,容易出错,特别是在处理连接、读取和写入等操作时。

5. 直接缓冲区的内存管理#

直接缓冲区是在堆外内存中分配的,它的创建和销毁成本较高,应该重用直接缓冲区,而不是频繁创建和销毁。

6. 内存映射文件的大小#

内存映射文件的大小不能超过系统的可用内存,否则会导致内存溢出。

7. 多线程安全#

NIO的通道和缓冲区不是线程安全的,在多线程环境中使用时,需要进行适当的同步。

总结#

NIO是Java中用于高性能I/O操作的API,它提供了非阻塞I/O、缓冲区、通道、选择器等核心组件,以及分散/聚集IO、内存映射文件等高级特性。NIO非常适合构建高性能的网络服务器和处理大文件I/O操作。

本文介绍了NIO的核心组件、基本操作和最佳实践。希望本文能够帮助你更好地理解和使用NIO。

练习#

  1. 编写一个NIO服务器,能够处理多个客户端的连接和请求。

  2. 编写一个NIO客户端,能够连接到服务器并发送和接收数据。

  3. 编写一个程序,使用NIO的文件通道复制一个大文件。

  4. 编写一个程序,使用NIO的内存映射文件技术读取和修改一个文件。

  5. 编写一个程序,使用NIO的管道在两个线程之间通信。

  6. 编写一个程序,使用NIO的分散/聚集IO技术读取和写入数据。

  7. 编写一个程序,比较传统IO和NIO的性能差异。

  8. 编写一个程序,使用NIO的选择器管理多个通道。

  9. 编写一个程序,使用NIO的直接缓冲区提高I/O性能。

  10. 编写一个程序,使用NIO的非阻塞IO处理多个连接。

通过这些练习,你将更加熟悉NIO的使用,为后续的学习做好准备。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
Java NIO详解
https://blog.vanilla.net.cn/posts/2026-02-05-java-nio/
作者
鹁鸪
发布于
2026-02-06
许可协议
CC BY-NC-SA 4.0

评论区

目录