目录
1、概念
Java NIO(New I/O)。它是在 Java 1.4 中引入的,旨在解决传统 Java I/O (java.io
包,也称为 BIO - Blocking I/O) 在高性能、高并发网络编程和文件操作方面的瓶颈。
Java NIO 的核心思想是基于 I/O 模型中的 I/O 多路复用 和 非阻塞 I/O。
NIO有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)。
NIO是面对缓冲区编程。
2、核心组件与概念
2.1 Buffer
(缓冲区)
角色: 数据的中转站。所有数据的读写都必须通过
Buffer
进行。它是 NIO 与 BIO 最显著的区别之一(BIO 直接操作流)。本质: 本质上是一个内存块(通常是堆外内存
DirectBuffer
,也可以是堆内内存HeapBuffer
),包装成一个对象,提供了一组方法来更轻松地操作这块内存。关键属性:
capacity
: 缓冲区的最大容量。创建后不可变。position
: 下一个要读取或写入的位置索引。初始为 0,随读写操作移动。limit
: 第一个不能读取或写入的位置索引。对于写模式,limit
通常等于capacity
;切换到读模式时,limit
被设置为position
的值(即写入了多少数据),position
重置为 0。mark
: 一个临时标记的位置,可以通过reset()
方法将position
恢复到mark
的位置。可选。
类型: 有各种基本类型对应的
Buffer
:ByteBuffer
,CharBuffer
,ShortBuffer
,IntBuffer
,LongBuffer
,FloatBuffer
,DoubleBuffer
。ByteBuffer
是最常用的,也是唯一能与Channel
直接交互的类型。
操作:
allocate(int capacity)
: 分配一个指定容量的缓冲区(通常是堆内)。allocateDirect(int capacity)
: 分配一个直接缓冲区(堆外),避免了一次从内核空间到用户空间的拷贝(在某些系统调用如FileChannel.transferTo/transferFrom
或网络读写时可能提高性能),但创建和销毁成本更高。put()
/get()
: 读写数据的方法族。flip()
: 将缓冲区从写模式切换到读模式。设置limit = position; position = 0;
。clear()
: 将缓冲区切换到写模式(准备重新写入)。设置position = 0; limit = capacity;
。不擦除数据,但新数据会覆盖旧数据。rewind()
: 将position
重置为 0,limit
不变。用于重新读取缓冲区的数据。compact()
: 将未读的数据(position
到limit
之间)复制到缓冲区的起始处,然后将position
设置为未读数据的下一个位置,limit
设置为capacity
。常用于在写模式下继续添加数据,同时保留未读数据。
2.2 Channel
(通道)
角色: 连接数据源(文件、Socket等)和
Buffer
的管道。是进行 I/O 操作的入口点。
与流的区别:
BIO 的
InputStream
/OutputStream
是单向的(输入/输出)。Channel
是双向的(可以读也可以写,但可能需要结合SelectableChannel
和Selector
才能实现真正的非阻塞)。Channel
总是与Buffer
交互,不能直接操作字节数组。Channel
支持非阻塞模式(核心特性之一)。
主要类型:
FileChannel
: 用于文件 I/O。不支持非阻塞模式。支持文件锁 (lock()/tryLock()
)、内存映射文件 (map()
) 和高效的零拷贝文件传输 (transferTo()/transferFrom()
)。SocketChannel
: 用于 TCP 网络通信(客户端和服务器端)。类似于Socket。ServerSocketChannel
: 用于 TCP 服务器端,监听传入的连接。accept()
方法返回SocketChannel
。类似于ServerSocket。DatagramChannel
: 用于 UDP 网络通信。
关键操作:
read(ByteBuffer dst)
: 从通道读取数据到缓冲区。在非阻塞模式下,可能返回 0(没有可用数据)。write(ByteBuffer src)
: 将缓冲区数据写入通道。在非阻塞模式下,可能只写入部分数据或返回 0(无法立即写入)。configureBlocking(boolean block)
: 设置通道为阻塞或非阻塞模式。register(Selector sel, int ops)
: 将通道注册到选择器上,并指定感兴趣的操作(SelectionKey.OP_READ
,OP_WRITE
,OP_CONNECT
,OP_ACCEPT
)。这是实现多路复用的关键。close()
: 关闭通道。
2.3 Selector
(选择器)
角色: NIO 多路复用的核心。一个
Selector
可以同时监控多个SelectableChannel
的 I/O 事件状态(如“连接就绪”、“读就绪”、“写就绪”)。 当某些通道准备好进行 I/O 操作时,Selector
会通知应用程序,避免了为每个连接创建一个线程进行阻塞等待。工作原理:
创建
Selector
(Selector.open()
)。将
SelectableChannel
(如SocketChannel
,ServerSocketChannel
) 设置为非阻塞模式 (channel.configureBlocking(false)
)。将通道注册到
Selector
上,并指定该通道感兴趣的操作 (SelectionKey.OP_READ | OP_WRITE
等)。注册会返回一个SelectionKey
对象,它代表了该通道在Selector
上的注册关系。调用
Selector.select()
方法(或selectNow()
,select(long timeout)
)。这个方法会阻塞(除非用selectNow()
),直到至少有一个注册的通道发生了它感兴趣的事件,或者超时,或者选择器的wakeup()
方法被调用。select()
返回后,调用selectedKeys()
方法获取已就绪的SelectionKey
集合。遍历这个
selectedKeys
集合。对于每个
SelectionKey
:检查它的事件类型 (
key.isAcceptable()
,key.isConnectable()
,key.isReadable()
,key.isWritable()
)。根据事件类型执行相应的操作(如
accept()
新连接、finishConnect()
、read()
、write()
)。非常重要: 处理完该
SelectionKey
后,必须将其从selectedKeys
集合中移除 (iterator.remove()
),否则下次select()
返回时它还会在集合里,导致重复处理。
回到第 4 步,继续监听。
SelectionKey
关键属性:channel()
: 获取关联的通道。selector()
: 获取关联的选择器。interestOps()
: 获取或设置该键感兴趣的操作集。readyOps()
: 获取通道已就绪的操作集。attach(Object ob)
: 附加一个对象(如会话状态、缓冲区)到键上。attachment()
: 获取附加的对象。
2.4 Charset
(字符集)
角色: 提供字节序列(
ByteBuffer
)和 Unicode 字符序列(CharBuffer
)之间的编码和解码功能。核心类:
Charset
: 代表一个字符集(如 "UTF-8", "GBK", "ISO-8859-1")。CharsetEncoder
: 将CharBuffer
编码为ByteBuffer
。CharsetDecoder
: 将ByteBuffer
解码为CharBuffer
。
使用: 在需要处理文本数据(如 HTTP 协议头、消息体)时,结合
Buffer
使用。
3、核心优势
高并发: 通过
Selector
实现单线程(或少量线程)管理大量连接(数千甚至数万),显著减少线程创建、上下文切换和内存开销。这是 NIO 最重要的优势。非阻塞 I/O: 通道可以设置为非阻塞模式。当没有数据可读或可写时,
read
/write
操作立即返回,允许线程执行其他任务(如处理其他就绪的通道)。避免了线程在 I/O 上的空闲等待。基于事件驱动: 程序逻辑围绕
Selector
检测到的 I/O 事件(读就绪、写就绪等)展开,符合网络编程的天然模式。零拷贝潜力:
FileChannel
的transferTo()
和transferFrom()
方法,以及MappedByteBuffer
(内存映射文件)可以在特定场景下减少数据在用户空间和内核空间之间的拷贝次数,提高大文件操作的效率。DirectBuffer
在特定系统调用下也有助于减少拷贝。更灵活的 Buffer 操作: 缓冲区提供了对数据更精细的控制(
position
,limit
,flip
,compact
等),便于处理如数据分包/粘包等复杂网络协议问题。
4、典型应用场景
高性能网络服务器: Web 服务器(如 Netty、Tomcat NIO Connector)、游戏服务器、即时通讯服务器、RPC 框架等。
需要处理大量并发连接的应用: 代理服务器、负载均衡器。
需要非阻塞 I/O 的文件操作: 虽然
FileChannel
本身阻塞,但结合Selector
可以和其他网络通道一起管理,或者利用其零拷贝特性提升效率。
5、编程模型与代码示例:简单 Echo 服务器
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
public class NioEchoServer {
public static void main(String[] args) throws IOException {
// 1. 创建 Selector
Selector selector = Selector.open();
// 2. 创建 ServerSocketChannel 并绑定端口
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.bind(new InetSocketAddress(8080));
serverSocket.configureBlocking(false); // 设置为非阻塞
// 3. 将 ServerSocketChannel 注册到 Selector,关注 ACCEPT 事件
serverSocket.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server started on port 8080...");
// 4. 事件循环
while (true) {
selector.select(); // 阻塞等待就绪事件
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectedKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove(); // 必须移除,防止重复处理
try {
if (key.isAcceptable()) { // 处理新连接
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false); // 新连接设置为非阻塞
System.out.println("Accepted connection from: " + client.getRemoteAddress());
// 注册新连接到 Selector,关注 READ 事件
client.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
} else if (key.isReadable()) { // 处理读事件
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
int bytesRead = client.read(buffer);
if (bytesRead == -1) { // 客户端关闭连接
System.out.println("Client closed connection.");
key.cancel();
client.close();
} else if (bytesRead > 0) {
buffer.flip(); // 切换到读模式
// 注册写事件,准备回写数据 (注意:这里直接注册写事件,更严谨的做法可能是先处理数据再注册写)
key.interestOps(SelectionKey.OP_WRITE);
}
} else if (key.isWritable()) { // 处理写事件
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
while (buffer.hasRemaining()) {
client.write(buffer); // 将缓冲区数据写回客户端
}
buffer.clear(); // 清空缓冲区,准备下次读
// 写完后重新关注 READ 事件
key.interestOps(SelectionKey.OP_READ);
}
} catch (IOException e) {
// 处理异常(如客户端异常断开)
key.cancel();
try {
key.channel().close();
} catch (IOException ex) {
ex.printStackTrace();
}
e.printStackTrace();
}
}
}
}
}
6、注意事项
空轮询 Bug (早期版本): 在某些 Linux 内核版本中,
Selector.select()
可能会在没有就绪事件的情况下立即返回(返回 0),导致 CPU 100%。这通常通过给select()
设置一个很小的超时或使用selectNow()
结合短暂休眠来缓解。现代 JVM 和操作系统通常已修复此问题。粘包/拆包: TCP 是字节流协议,没有消息边界。NIO 的
read
操作可能一次读取多条消息的一部分(粘包),或者一条消息被分多次读取(拆包)。应用程序必须在Buffer
层面实现自己的协议解析(如基于长度、分隔符等)来处理这个问题。这是 NIO 网络编程中最大的挑战之一。并发控制: 虽然
Selector
通常运行在单线程,但处理SelectionKey
事件(尤其是耗时的业务逻辑)时,如果处理不当,可能会阻塞整个事件循环。通常需要将耗时的 I/O(如数据库访问)或计算任务交给线程池处理。内存管理:
DirectBuffer
分配和释放成本较高,管理不当可能导致内存泄漏或OutOfMemoryError: Direct buffer memory
。需要谨慎使用并考虑池化。需要小心管理
Buffer
的大小和生命周期,避免内存浪费。
资源关闭: 务必确保
Channel
、Selector
、ServerSocketChannel
等资源在使用完毕后正确关闭 (close()
),通常在finally
块中处理。