Java NIO 详解
Java NIO(New IO)是 JDK 1.4 引入的非阻塞 I/O 模型 ,核心目标是解决传统 BIO(Blocking IO)在高并发场景下的性能瓶颈。它基于「通道(Channel)+ 缓冲区(Buffer)+ 选择器(Selector)」的设计,支持非阻塞、面向缓冲区、多路复用,广泛用于高并发网络编程(如服务器、中间件)和高效文件操作。
一、Java NIO 核心特性
与传统 BIO 对比,NIO 的核心优势:
| 特性 | Java BIO(传统 IO) | Java NIO |
|---|---|---|
| 数据操作方式 | 面向流(Stream),单向 | 面向缓冲区(Buffer),双向 |
| 阻塞特性 | 同步阻塞(Blocking) | 同步非阻塞(Non-Blocking)+ 多路复用 |
| 线程模型 | 一个连接一个线程(高开销) | 单线程 / 少量线程管理多连接 |
| 适用场景 | 低并发、简单 IO 操作 | 高并发、高吞吐量场景(如 Netty 底层) |
二、NIO 三大核心组件
1. 缓冲区(Buffer):数据的容器
所有 NIO 操作都通过缓冲区进行 ------ 数据从通道(Channel)读取到缓冲区,或从缓冲区写入通道。Buffer 是一个固定大小的数组,支持基本数据类型(ByteBuffer、CharBuffer、IntBuffer 等),核心是通过 4 个属性控制数据读写:
| 属性 | 作用 | 读写模式切换逻辑 |
|---|---|---|
capacity |
缓冲区最大容量(创建后不可变) | - |
position |
当前读写位置(初始为 0) | 写时自增,读时通过 flip() 重置为 0 |
limit |
读写的边界(写时 = capacity,读时 = 写的总长度) | flip() 后设为当前 position |
mark |
标记一个位置,用于 reset() 回退 |
可选,需配合 mark() 和 reset() |
Buffer 核心方法
put():写入数据到缓冲区(position 自增);get():从缓冲区读取数据(position 自增);flip():切换为读模式(limit = position,position = 0);rewind():重置读位置(position = 0,limit 不变);clear():清空缓冲区(重置 position=0、limit=capacity,数据未真正删除);compact():压缩缓冲区(保留未读取数据,将其移到开头,position 指向数据末尾)。
示例:ByteBuffer 基本使用
java
import java.nio.ByteBuffer;
public class BufferDemo {
public static void main(String[] args) {
// 1. 创建缓冲区(容量 10 字节)
ByteBuffer buffer = ByteBuffer.allocate(10);
System.out.println("初始化:" + buffer); // position=0, limit=10, capacity=10
// 2. 写入数据(写模式)
buffer.put("hello".getBytes());
System.out.println("写入后:" + buffer); // position=5, limit=10, capacity=10
// 3. 切换为读模式
buffer.flip();
System.out.println("flip 后:" + buffer); // position=0, limit=5, capacity=10
// 4. 读取数据
byte[] dest = new byte[buffer.limit()];
buffer.get(dest);
System.out.println("读取内容:" + new String(dest)); // hello
System.out.println("读取后:" + buffer); // position=5, limit=5, capacity=10
// 5. 重置读位置(重新读取)
buffer.rewind();
System.out.println("rewind 后:" + buffer); // position=0, limit=5, capacity=10
}
}
2. 通道(Channel):数据的传输通道
Channel 是 NIO 中双向的数据传输通道 (可读可写,区别于 BIO 流的单向),必须配合 Buffer 操作。通道是非阻塞的 (可通过 configureBlocking(false) 设置),支持异步读写。
常用 Channel 实现类
| 通道类型 | 作用 | 典型场景 |
|---|---|---|
FileChannel |
文件 IO 操作(读 / 写文件) | 大文件传输、文件复制 |
SocketChannel |
TCP 客户端通道(连接服务器、读写数据) | 高并发客户端通信 |
ServerSocketChannel |
TCP 服务器通道(监听端口、接收连接) | 高并发服务器(如 NIO 服务器) |
DatagramChannel |
UDP 通道(无连接数据传输) | 实时通信(如游戏、直播) |
示例:FileChannel 复制文件(高效)
java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.channels.FileChannel;
public class FileChannelDemo {
public static void main(String[] args) throws Exception {
// 源文件和目标文件
try (FileInputStream fis = new FileInputStream("source.txt");
FileOutputStream fos = new FileOutputStream("target.txt");
// 获取通道
FileChannel inChannel = fis.getChannel();
FileChannel outChannel = fos.getChannel()) {
// 直接传输(零拷贝,效率极高)
inChannel.transferTo(0, inChannel.size(), outChannel);
// 或 outChannel.transferFrom(inChannel, 0, inChannel.size());
}
System.out.println("文件复制完成");
}
}
3. 选择器(Selector):多路复用的核心
Selector 是 NIO 实现单线程管理多通道的关键,本质是「事件监听器」------ 一个线程通过 Selector 监听多个 Channel 的就绪事件(如连接就绪、读就绪、写就绪),仅当通道有事件时才处理,避免了 BIO 中线程阻塞等待的开销。
核心概念
-
注册通道 :Channel 必须先注册到 Selector,且设置为非阻塞模式 (
channel.configureBlocking(false)); -
事件类型
:Selector 监听的 4 种事件(用
SelectionKey表示):SelectionKey.OP_ACCEPT:接收连接就绪(ServerSocketChannel 专属);SelectionKey.OP_CONNECT:连接就绪(SocketChannel 专属);SelectionKey.OP_READ:读就绪(通道有数据可读);SelectionKey.OP_WRITE:写就绪(通道可写入数据);
-
SelectionKey:通道注册到 Selector 后返回的「事件句柄」,包含通道、选择器、事件类型等信息。
Selector 工作流程
- 创建 Selector;
- 通道设置为非阻塞模式,注册到 Selector 并指定监听事件;
- 调用
selector.select()阻塞等待就绪事件(返回就绪事件数); - 遍历就绪的
SelectionKey集合,处理对应事件(如接收连接、读数据); - 移除已处理的
SelectionKey,重复步骤 3。
示例:NIO 单线程 TCP 服务器
java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NioTcpServer {
public static void main(String[] args) throws IOException {
// 1. 创建 Selector
Selector selector = Selector.open();
// 2. 创建 ServerSocketChannel 并绑定端口
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false); // 非阻塞模式
// 3. 注册到 Selector,监听「接收连接」事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO 服务器启动,监听端口 8080...");
while (true) {
// 4. 阻塞等待就绪事件(返回就绪事件数)
int readyChannels = selector.select();
if (readyChannels == 0) continue;
// 5. 遍历就绪的 SelectionKey
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 处理「接收连接」事件(ServerSocketChannel)
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = server.accept(); // 接收客户端连接
clientChannel.configureBlocking(false); // 客户端通道非阻塞
// 注册客户端通道到 Selector,监听「读就绪」事件
clientChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
System.out.println("客户端连接:" + clientChannel.getRemoteAddress());
}
// 处理「读就绪」事件(SocketChannel)
if (key.isReadable()) {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment(); // 获取绑定的缓冲区
// 读取客户端数据
int readLen = clientChannel.read(buffer);
if (readLen == -1) { // 客户端断开连接
clientChannel.close();
key.cancel();
System.out.println("客户端断开连接:" + clientChannel.getRemoteAddress());
continue;
}
// 切换读模式,打印数据
buffer.flip();
byte[] data = new byte[buffer.limit()];
buffer.get(data);
System.out.println("收到数据:" + new String(data) + "(来自:" + clientChannel.getRemoteAddress() + ")");
// 重置缓冲区,准备下次读取
buffer.clear();
}
// 移除已处理的 Key(避免重复处理)
iterator.remove();
}
}
}
}
三、NIO 其他重要组件
1. 管道(Pipe):线程间通信
Pipe 是 NIO 提供的单向管道,用于两个线程间的通信,包含两个通道:
Pipe.SourceChannel:读通道(接收数据);Pipe.SinkChannel:写通道(发送数据)。
示例:Pipe 线程通信
java
import java.nio.ByteBuffer;
import java.nio.channels.Pipe;
public class PipeDemo {
public static void main(String[] args) throws Exception {
// 创建管道
Pipe pipe = Pipe.open();
// 写线程:向 SinkChannel 写入数据
new Thread(() -> {
try {
Pipe.SinkChannel sinkChannel = pipe.sink();
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Hello from Pipe!".getBytes());
buffer.flip();
sinkChannel.write(buffer);
sinkChannel.close();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
// 读线程:从 SourceChannel 读取数据
new Thread(() -> {
try {
Pipe.SourceChannel sourceChannel = pipe.source();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int readLen = sourceChannel.read(buffer);
buffer.flip();
System.out.println("读取到数据:" + new String(buffer.array(), 0, readLen));
sourceChannel.close();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
2. 文件锁(FileLock):并发文件安全
FileChannel 支持文件锁,用于多线程 / 多进程并发操作文件时的同步:
java
// 获取文件锁(独占锁)
FileLock lock = fileChannel.lock();
// 或获取共享锁(仅读)
// FileLock lock = fileChannel.lock(0, FileChannel.MapMode.READ_ONLY, true);
// 操作文件...
lock.release(); // 释放锁
3. 字符集(Charsets):编码解码
NIO 提供 Charsets 类处理字符编码(如 UTF-8、GBK),配合 CharBuffer 和 ByteBuffer 实现字符串与字节的转换:
java
import java.nio.charset.Charset;
import java.nio.CharBuffer;
import java.nio.ByteBuffer;
public class CharsetDemo {
public static void main(String[] args) {
Charset utf8 = Charset.forName("UTF-8");
// 字符串 → 字节(编码)
String str = "你好,NIO";
ByteBuffer byteBuffer = utf8.encode(str);
// 字节 → 字符串(解码)
CharBuffer charBuffer = utf8.decode(byteBuffer);
System.out.println(charBuffer.toString()); // 你好,NIO
}
}
四、NIO 适用场景与注意事项
适用场景
- 高并发网络编程:如服务器、网关(如 Netty、Tomcat 8+ 底层);
- 高效文件操作:大文件复制、分片传输(FileChannel 零拷贝);
- 线程间通信:Pipe 替代传统的 Thread.join () 或队列,更高效。
注意事项
- 通道必须设置为非阻塞模式才能注册到 Selector;
selector.select()是阻塞方法,可通过select(long timeout)设置超时,或wakeup()唤醒;- Buffer 的
flip()、clear()等方法容易用错,需注意读写模式切换; - 非阻塞 IO 需处理「部分读写」(如
channel.read(buffer)可能只读取部分数据)。
五、总结
Java NIO 以「通道 + 缓冲区 + 选择器」为核心,通过非阻塞 + 多路复用解决了 BIO 高并发瓶颈,是高性能 Java 编程的基础。实际开发中,直接使用 NIO API 较为繁琐,通常会基于封装后的框架(如 Netty、Mina)进行开发,但理解 NIO 核心原理是掌握这些框架的关键。