一、NIO 多路复用基础概念
NIO 多路复用
概念定义
NIO(Non-blocking I/O)多路复用是一种高效的 I/O 处理机制,允许单个线程同时监控多个 I/O 通道(如 Socket 连接),并在通道就绪时进行读写操作。核心思想是通过一个系统调用(如 select
、poll
、epoll
)监听多个文件描述符(FD),避免为每个连接创建独立线程的开销。
核心组件
- Selector(选择器):核心对象,用于注册多个
Channel
并监听其 I/O 事件(如读、写、连接)。 - Channel(通道):双向数据传输管道(如
SocketChannel
、ServerSocketChannel
),需配置为非阻塞模式。 - SelectionKey:标识
Channel
与Selector
的注册关系,包含就绪事件类型(OP_READ
、OP_WRITE
等)。
工作流程
- 创建
Selector
并将Channel
注册到其中。 - 调用
Selector.select()
阻塞等待就绪事件。 - 遍历
selectedKeys()
处理就绪的Channel
。 - 执行实际 I/O 操作后,移除已处理的
Key
。
使用场景
- 高并发网络服务(如 Web 服务器、即时通讯)。
- 需要管理大量长连接的场景(如游戏服务器)。
- 资源受限环境(减少线程数)。
优势
- 资源高效:单线程处理多连接,减少线程上下文切换。
- 延迟低:避免阻塞等待,快速响应就绪事件。
- 扩展性强:适合 C10K 甚至更高并发问题。
示例代码(Java NIO)
// 创建 Selector 和 ServerSocketChannel
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); // 阻塞等待就绪事件
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isAcceptable()) {
// 处理新连接
SocketChannel client = serverChannel.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 处理读事件
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer);
// 处理数据...
}
}
}
注意事项
- 线程安全:
Selector
本身线程安全,但注册的Channel
需自行保证。 - 事件清理:处理完
SelectionKey
后必须手动remove()
,否则会重复触发。 - 空轮询问题:某些系统(如 Linux)下
select()
可能意外返回 0,需通过超时或改用epoll
规避。
常见误区
- 误用阻塞模式:未调用
configureBlocking(false)
会导致多路复用失效。 - 忽略写事件:仅在缓冲区满时才注册
OP_WRITE
,避免持续占用 CPU。 - 过度依赖单线程:复杂业务逻辑仍需配合线程池处理。
多路复用的核心思想
多路复用(Multiplexing)的核心思想是用一个线程/进程监控多个I/O流,当其中任意一个流就绪(可读/可写)时,线程就能立即处理,避免阻塞等待。本质上是通过事件驱动的方式实现高效I/O管理。
关键点解析
-
单线程管理多通道
传统BIO中每个连接需独立线程处理,而多路复用通过Selector
机制,单线程即可轮询所有注册的通道(Channel),仅处理活跃事件。 -
事件驱动模型
基于OP_READ
/OP_WRITE
等事件通知,而非主动轮询。当内核检测到某通道事件就绪,会触发Selector返回对应就绪集合。 -
非阻塞基础
所有通道必须配置为非阻塞模式(configureBlocking(false)
),确保单线程在等待事件时不阻塞其他通道的处理。
核心优势
- 资源高效:减少线程上下文切换开销(对比BIO的1:1线程模型)
- 高并发支撑:C10K问题下性能显著优于阻塞IO
- 延迟优化:就绪事件即时响应,避免轮询空转
类比说明
类似餐厅服务员场景:
- BIO:每个顾客(连接)配专属服务员(线程),空闲时也占用资源。
- 多路复用:一个服务员监听多个餐桌(通道),谁需要服务(事件就绪)就处理谁。
传统 IO 模型(BIO)与 NIO 多路复用的核心区别
阻塞 vs 非阻塞
- BIO:线程在
read()
/write()
时会阻塞,直到数据就绪。 - NIO:通过
Selector
实现非阻塞,线程仅在有事件(如可读/可写)时被唤醒。
线程模型
- BIO:1 连接 = 1 线程(高并发时线程爆炸)。
- NIO:1 线程可处理多个连接(通过
Selector
轮询事件)。
数据就绪检测
- BIO:依赖内核阻塞等待数据。
- NIO:通过
Selector
主动查询就绪的通道(基于epoll
/kqueue
等系统调用)。
代码示例对比
// BIO 服务端(伪代码)
while (true) {
Socket client = serverSocket.accept(); // 阻塞
new Thread(() -> {
InputStream in = client.getInputStream();
in.read(); // 阻塞读取
}).start();
}
// NIO 服务端(伪代码)
Selector selector = Selector.open();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
while (true) {
selector.select(); // 阻塞直到有事件
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
channel.read(buffer); // 非阻塞读取
}
}
}
适用场景
- BIO:连接数少、逻辑简单的场景(如传统 HTTP 服务器)。
- NIO:高并发、长连接场景(如聊天服务器、RPC 框架)。
关键差异总结
特性 | BIO | NIO |
---|---|---|
阻塞方式 | 线程阻塞 | 事件驱动 |
线程开销 | 高(1连接1线程) | 低(多路复用) |
吞吐量 | 低 | 高 |
编程复杂度 | 简单 | 较高(需处理事件) |
适用场景分析
高并发网络应用
- Web服务器:如Tomcat、Netty等,处理大量客户端连接时,通过多路复用减少线程开销。
- 即时通讯:聊天服务器需同时维持成千上万的TCP长连接,NIO可高效管理连接状态。
- 游戏服务器:低延迟需求下,快速响应多个玩家的实时操作。
I/O密集型服务
- 文件传输:处理大量文件上传/下载时,避免阻塞线程。
- 代理服务:如反向代理(Nginx)、数据库中间件,需同时转发多路数据流。
特殊硬件环境
- 嵌入式系统:资源受限设备中,减少内存和CPU占用。
- 物联网网关:管理海量设备连接时,单机支撑更高并发。
与传统方案的对比场景
场景 | BIO(阻塞式)适用性 | NIO多路复用适用性 |
---|---|---|
连接数 < 1000 | ✅ 开发简单 | ⚠️ 过度设计 |
连接数 > 5000 | ❌ 线程爆炸 | ✅ 资源可控 |
短连接高频请求 | ❌ 频繁创建线程 | ✅ 复用连接 |
长连接低活跃度 | ❌ 线程空等 | ✅ 事件驱动唤醒 |
不适用场景
- 简单低频应用:如内部管理后台,开发效率优先时。
- CPU密集型任务:多路复用无法加速计算过程。
- Windows平台AIO:建议直接使用IOCP(CompletionPort)而非Java NIO。
Selector(选择器)
定义
Selector 是 Java NIO 的核心组件之一,用于监听多个 Channel 上的 I/O 事件(如连接、读、写)。它通过单线程高效管理多个 Channel,实现多路复用。
使用场景
- 高并发网络服务器(如 Web 服务器、游戏服务器)
- 需要同时处理大量连接的场景(如聊天室、实时数据传输)
关键方法
open()
:创建 Selectorselect()
:阻塞等待就绪的 ChannelselectedKeys()
:获取已就绪的 Channel 集合
示例代码
Selector selector = Selector.open();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
Channel(通道)
定义
Channel 是 NIO 中用于传输数据的双向管道,支持异步非阻塞 I/O 操作。与传统的流(Stream)不同,Channel 可以同时进行读写。
常见实现类
SocketChannel
:TCP 客户端通道ServerSocketChannel
:TCP 服务端通道FileChannel
:文件操作通道
特点
- 非阻塞模式(通过
configureBlocking(false)
设置) - 支持 Scatter/Gather 操作(分散读/聚集写)
示例代码
SocketChannel clientChannel = SocketChannel.open();
clientChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
Buffer(缓冲区)
定义
Buffer 是 Channel 读写数据时的临时存储容器,本质是一个定长的数组。NIO 的所有数据操作都通过 Buffer 完成。
核心属性
capacity
:缓冲区最大容量position
:当前读写位置limit
:可操作数据边界
常见类型
ByteBuffer
(最常用)CharBuffer
/IntBuffer
等基本类型缓冲区
操作流程
- 写入数据到 Buffer(
put()
) - 调用
flip()
切换为读模式 - 从 Buffer 读取数据(
get()
) - 调用
clear()
或compact()
重置
示例代码
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Hello".getBytes());
buffer.flip();
channel.write(buffer);
二、Selector 详解
Selector 的核心作用
概念定义
Selector(选择器)是 Java NIO 中的一个核心组件,用于监控多个 Channel(通道)的 I/O 事件(如连接就绪、读就绪、写就绪等)。通过 Selector,单个线程可以高效管理多个 Channel,实现多路复用的 I/O 操作。
核心功能
-
事件监听
Selector 可以监听多个 Channel 上的事件(通过SelectionKey
标识),包括:OP_ACCEPT
:服务端接收连接就绪OP_CONNECT
:客户端连接就绪OP_READ
:数据可读OP_WRITE
:数据可写
-
非阻塞 I/O 多路复用
通过select()
方法阻塞等待事件发生,或通过selectNow()
非阻塞检查事件。当事件发生时,Selector 返回就绪的 Channel 集合,线程只需处理这些 Channel,避免空轮询。 -
单线程管理多 Channel
减少线程切换开销,解决传统 BIO 中“一连接一线程”的资源浪费问题。
使用场景
- 高并发服务器(如 Web 服务器、游戏服务器)
- 需要同时处理大量网络连接的场景
- 需要低延迟响应的应用(如实时通信)
示例代码
// 创建 Selector
Selector selector = Selector.open();
// 将 Channel 注册到 Selector,监听读事件
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 阻塞等待就绪事件
selector.select();
// 获取就绪的 Channel 集合
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
if (key.isAcceptable()) {
// 处理连接事件
} else if (key.isReadable()) {
// 处理读事件
}
iter.remove(); // 移除已处理的 Key
}
}
注意事项
-
Channel 必须为非阻塞模式
configureBlocking(false)
需在注册前调用,否则会抛IllegalBlockingModeException
。 -
正确清理 SelectionKey
处理完事件后需调用iter.remove()
,否则下次select()
会重复处理。 -
线程安全性
Selector 本身是线程安全的,但关联的SelectionKey
和 Channel 可能不是。 -
性能瓶颈
select()
是单线程的,若事件处理逻辑耗时过长,会影响整体吞吐量。
Selector 的创建
创建方式
Selector selector = Selector.open();
- 通过静态方法
Selector.open()
创建 - 底层调用系统默认的
SelectorProvider
实现
注意事项
- 一个进程可创建多个 Selector
- 单线程环境建议使用单个 Selector
- 多线程环境可为每个线程创建独立 Selector
Selector 的关闭
关闭方法
selector.close();
关闭特性
- 会释放所有关联的 Channel
- 会取消所有已注册的键(SelectionKey)
- 关闭后任何操作都会抛出
ClosedSelectorException
最佳实践
try (Selector selector = Selector.open()) {
// 使用selector
} // 自动关闭
- 推荐使用 try-with-resources 语法
- 确保资源及时释放
- 避免忘记关闭导致资源泄漏
状态检查
if (selector.isOpen()) {
// selector可用状态
}
- 操作前可检查 Selector 状态
- 避免在关闭状态执行操作
注册 Channel 到 Selector
概念定义
将 Channel 注册到 Selector 是多路复用 I/O 的核心操作之一。它允许 Selector 监控该 Channel 上的 I/O 事件(如可读、可写、连接就绪等)。
关键方法
SelectionKey register(Selector sel, int ops)
SelectionKey register(Selector sel, int ops, Object att)
sel
: 目标 Selectorops
: 感兴趣的事件集合(通过SelectionKey
常量组合)att
: 可选的附件对象
事件类型常量
常量 | 说明 |
---|---|
OP_READ | 读就绪 |
OP_WRITE | 写就绪 |
OP_CONNECT | 连接就绪 |
OP_ACCEPT | 接受就绪 |
使用示例
// 创建 Selector
Selector selector = Selector.open();
// 创建 ServerSocketChannel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false); // 必须设为非阻塞
serverChannel.bind(new InetSocketAddress(8080));
// 注册到 Selector,监听 ACCEPT 事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
注意事项
- 非阻塞模式:Channel 必须配置为非阻塞模式才能注册
- 事件组合:可以通过位或操作组合多个事件
channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
- 重复注册:对已注册的 Channel 再次调用 register() 会更新其监听事件
- 附件对象:可以通过附件传递上下文信息
channel.register(selector, SelectionKey.OP_READ, new MyAttachment());
返回值
返回的 SelectionKey
对象包含:
- 关联的 Channel 和 Selector
- 感兴趣的事件集合
- 就绪的事件集合
- 附件对象(如有)
SelectionKey 的作用
SelectionKey 是 Java NIO 中用于表示一个通道(Channel)在 Selector 上注册的标记。它包含了以下核心信息:
- 通道与选择器的关联关系:记录哪个 Channel 注册到了哪个 Selector。
- 监听事件类型:如 OP_READ、OP_WRITE、OP_CONNECT、OP_ACCEPT。
- 就绪事件集合:表示哪些事件已经就绪(可通过 readyOps() 获取)。
- 附加对象(Attachment):可绑定任意对象(如 Buffer、业务对象等)。
SelectionKey 的核心方法
1. 事件相关方法
int interestOps() // 获取当前关注的事件集合
SelectionKey interestOps(int ops) // 修改关注的事件
int readyOps() // 获取已就绪的事件集合
2. 通道与选择器
SelectableChannel channel() // 获取关联的通道
Selector selector() // 获取关联的选择器
3. 附件操作
Object attach(Object ob) // 绑定附件
Object attachment() // 获取附件
使用场景示例
基础注册示例
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
Selector selector = Selector.open();
// 注册时指定关注的事件和附件
SelectionKey key = channel.register(selector,
SelectionKey.OP_READ | SelectionKey.OP_WRITE,
new AttachmentObject());
事件处理模板
while (selector.select() > 0) {
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
if (key.isReadable()) {
// 处理读事件
SocketChannel ch = (SocketChannel) key.channel();
ByteBuffer buf = (ByteBuffer) key.attachment();
ch.read(buf);
}
if (key.isWritable()) {
// 处理写事件
}
}
}
注意事项
-
及时移除已处理的Key:必须调用
iterator.remove()
清除已处理的 Key,否则会重复处理。 -
不要阻塞事件循环:事件处理逻辑应当快速完成,长时间操作应交给线程池。
-
合理使用附件:
- 附件对象建议是不可变或线程安全的
- 典型用法:关联 Buffer 或会话状态对象
-
正确修改interestOps:
// 正确方式(保留原有事件) key.interestOps(key.interestOps() | SelectionKey.OP_WRITE); // 错误方式(会覆盖原有事件) key.interestOps(SelectionKey.OP_WRITE);
-
资源释放:调用
key.cancel()
不会立即注销,需在下一次 select 操作时生效。
监听事件类型
OP_ACCEPT
- 定义:表示服务器套接字通道准备好接受新连接的事件类型。
- 使用场景:用于
ServerSocketChannel
,当有新的客户端连接请求到达时触发。 - 注意事项:
- 仅适用于
ServerSocketChannel
- 触发后必须调用
accept()
获取新的SocketChannel
- 仅适用于
- 示例:
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
OP_CONNECT
- 定义:表示客户端套接字通道已完成或未能完成连接操作的事件类型。
- 使用场景:用于
SocketChannel
的非阻塞连接过程。 - 注意事项:
- 触发后必须检查
finishConnect()
- 连接成功后需取消该监听
- 触发后必须检查
- 示例:
socketChannel.register(selector, SelectionKey.OP_CONNECT);
OP_READ
- 定义:表示通道已准备好读取数据的事件类型。
- 使用场景:当通道中有数据可读时触发。
- 注意事项:
- 可能触发多次,需处理半包/粘包问题
- 读取返回-1表示对端关闭
- 示例:
socketChannel.register(selector, SelectionKey.OP_READ);
OP_WRITE
- 定义:表示通道已准备好写入数据的事件类型。
- 使用场景:当通道可写时触发(通常内核缓冲区有空闲空间)。
- 注意事项:
- 不应长期注册,会导致CPU空转
- 应在需要写入时注册,写入完成后取消
- 示例:
socketChannel.register(selector, SelectionKey.OP_WRITE);
组合使用
- 位运算组合:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE
- 检查方法:
key.isReadable()
,key.isWritable()
- 修改监听:
key.interestOps(newInterestSet)
三、Channel 详解
Channel 的基本特性
定义
Channel(通道)是 Java NIO 的核心组件之一,用于在缓冲区(Buffer)和数据源/目标(如文件、套接字等)之间高效传输数据。与传统的 I/O 流不同,Channel 是双向的(可读可写),且支持异步非阻塞操作。
核心特性
-
双向性
- 不同于
InputStream
/OutputStream
的单向操作,Channel 可同时支持读写(如SocketChannel
、FileChannel
)。 - 例外:部分 Channel 是单向的(如
FileChannel
需通过mode
参数指定读写权限)。
- 不同于
-
非阻塞模式
- Channel 可设置为非阻塞模式(通过
configureBlocking(false)
),此时读写操作不会阻塞线程,适合高并发场景。 - 典型应用:结合
Selector
实现多路复用(如ServerSocketChannel
监听客户端连接)。
- Channel 可设置为非阻塞模式(通过
-
基于 Buffer 操作
- 所有数据必须通过 Buffer 与 Channel 交互(
read(Buffer)
/write(Buffer)
),避免直接操作字节数组。 - Buffer 提供结构化数据访问(如位置、限制等指针控制)。
- 所有数据必须通过 Buffer 与 Channel 交互(
-
分散(Scatter)/聚集(Gather)
- ScatterRead:从 Channel 读取数据到多个 Buffer(按顺序填充)。
- GatherWrite:将多个 Buffer 的数据按顺序写入 Channel。
- 示例代码:
ByteBuffer header = ByteBuffer.allocate(128); ByteBuffer body = ByteBuffer.allocate(1024); ByteBuffer[] buffers = {header, body}; channel.read(buffers); // ScatterRead channel.write(buffers); // GatherWrite
-
内存映射文件(FileChannel 特有)
- 通过
map()
方法将文件直接映射到内存(MappedByteBuffer
),避免传统文件 I/O 的拷贝开销。 - 示例代码:
FileChannel channel = FileChannel.open(Path.of("data.txt")); MappedByteBuffer buffer = channel.map( FileChannel.MapMode.READ_WRITE, 0, channel.size());
- 通过
常见实现类
类名 | 用途 |
---|---|
FileChannel | 文件读写(需通过 RandomAccessFile 或 FileInputStream /FileOutputStream 获取) |
SocketChannel | TCP 套接字通信(客户端/服务端) |
ServerSocketChannel | 监听 TCP 连接(服务端) |
DatagramChannel | UDP 数据报通信 |
注意事项
-
资源释放
Channel 需显式调用close()
或通过try-with-resources
关闭,避免资源泄漏。try (SocketChannel channel = SocketChannel.open()) { // 操作 Channel }
-
线程安全
Channel 不是线程安全的,多线程环境下需自行同步(如通过锁或单线程模型)。 -
性能优化
- 对于频繁的小数据操作,复用 Buffer 而非频繁创建。
- 文件操作优先使用内存映射(
MappedByteBuffer
)或transferTo()
/transferFrom()
零拷贝方法。
示例代码(非阻塞模式)
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false); // 设置为非阻塞
channel.connect(new InetSocketAddress("example.com", 80));
while (!channel.finishConnect()) {
// 等待连接完成(非阻塞模式下立即返回)
}
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = channel.read(buffer); // 非阻塞读取(可能返回0)
主要 Channel 类型:SocketChannel 和 ServerSocketChannel
概念定义
-
SocketChannel
- 用于 TCP 网络通信的客户端或服务端通道,支持非阻塞模式。
- 可读写数据,直接与对端 Socket 交互。
-
ServerSocketChannel
- 专用于服务端监听 TCP 连接的通道,类似传统
ServerSocket
。 - 通过
accept()
方法接收新连接,返回SocketChannel
对象。
- 专用于服务端监听 TCP 连接的通道,类似传统
核心区别
特性 | SocketChannel | ServerSocketChannel |
---|---|---|
用途 | 数据传输 | 监听连接 |
关键方法 | read() , write() | accept() |
是否支持非阻塞 | 是 | 是 |
使用场景
-
SocketChannel
// 客户端示例 SocketChannel clientChannel = SocketChannel.open(); clientChannel.connect(new InetSocketAddress("localhost", 8080)); ByteBuffer buffer = ByteBuffer.wrap("Hello".getBytes()); clientChannel.write(buffer);
-
ServerSocketChannel
// 服务端示例 ServerSocketChannel serverChannel = ServerSocketChannel.open(); serverChannel.bind(new InetSocketAddress(8080)); serverChannel.configureBlocking(false); // 非阻塞模式 SocketChannel clientChannel = serverChannel.accept(); // 需配合Selector使用
注意事项
- 非阻塞模式必须显式设置
channel.configureBlocking(false); // 否则无法配合Selector使用
- 资源释放
操作完成后需调用close()
关闭通道,避免资源泄漏。 - 连接检查
SocketChannel.finishConnect()
需在非阻塞模式下检查连接是否完成。
常见误区
- 误将
ServerSocketChannel
用于数据传输(实际需通过accept()
获取SocketChannel
操作)。 - 未设置非阻塞模式导致
accept()
或read()
阻塞线程。
Channel 的阻塞/非阻塞模式
概念定义
在 Java NIO 中,Channel
是数据源和数据目标之间的通道,支持**阻塞模式(Blocking Mode)和非阻塞模式(Non-blocking Mode)**两种配置:
- 阻塞模式:I/O 操作(如
read()
、write()
)会阻塞线程,直到操作完成或发生错误。 - 非阻塞模式:I/O 操作立即返回,若数据未就绪,返回
0
或null
,线程可继续执行其他任务。
配置方法
通过 configureBlocking(boolean block)
方法设置:
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false); // 设置为非阻塞模式
使用场景
-
阻塞模式:
- 适用于简单、同步的 I/O 操作(如传统 Socket 编程)。
- 代码逻辑直观,但会阻塞线程,不适合高并发场景。
-
非阻塞模式:
- 需配合
Selector
实现多路复用(如 Reactor 模式)。 - 适合高并发、低延迟场景(如聊天服务器、文件传输)。
- 需配合
注意事项
- 仅在连接后配置:对
SocketChannel
,需在connect()
前设置非阻塞模式,否则可能抛出IllegalBlockingModeException
。 - 性能权衡:
- 阻塞模式线程上下文切换少,但吞吐量低。
- 非阻塞模式需额外轮询(如
Selector
),但可处理更多连接。
- 缓冲区处理:非阻塞模式下,
read()
可能只读取部分数据,需检查返回值并多次调用。
示例代码
// 非阻塞模式示例
try (ServerSocketChannel serverChannel = ServerSocketChannel.open()) {
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false); // 非阻塞模式
Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); // 阻塞直到有就绪事件
Set<SelectionKey> keys = selector.selectedKeys();
// 处理就绪的 Channel...
}
}
常见误区
- 混淆模式与超时:阻塞模式不支持超时控制,需用非阻塞模式 +
Selector
实现超时检测。 - 未处理部分写入:非阻塞模式下
write()
可能只写入部分数据,需循环调用直到缓冲区为空。
Channel 的注册与注销
概念定义
在 Java NIO 中,Channel
的注册与注销是指将一个 Channel
绑定到 Selector
上或从 Selector
上移除的过程。通过注册,Selector
可以监听该 Channel
上的 I/O 事件(如读、写、连接等)。注销则是取消这种监听关系。
使用场景
- 注册:当需要监听某个
Channel
的 I/O 事件时,将其注册到Selector
上。 - 注销:当不再需要监听该
Channel
的事件时(如连接关闭),将其从Selector
上注销。
注册方法
通过 Channel.register(Selector selector, int ops)
方法注册,其中:
selector
:要注册的Selector
实例。ops
:监听的事件类型(如SelectionKey.OP_READ
、SelectionKey.OP_WRITE
等)。
// 示例:将 SocketChannel 注册到 Selector 上,监听读事件
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false); // 必须为非阻塞模式
Selector selector = Selector.open();
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
注销方法
- 显式注销:调用
SelectionKey.cancel()
方法。 - 隐式注销:当
Channel
关闭时,会自动注销。
// 显式注销
key.cancel();
// 隐式注销(关闭 Channel 时自动注销)
channel.close();
注意事项
- 非阻塞模式:注册前必须将
Channel
设置为非阻塞模式(configureBlocking(false)
),否则会抛出IllegalBlockingModeException
。 - 重复注册:同一个
Channel
可以多次注册到不同的Selector
,但每次注册会返回一个新的SelectionKey
。 - 事件类型:注册时可以同时监听多个事件,用
|
连接(如OP_READ | OP_WRITE
)。 - 资源释放:注销后,
SelectionKey
不会立即从Selector
中移除,需调用Selector.select()
后才会生效。
示例代码
// 注册与注销完整示例
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
Selector selector = Selector.open();
// 注册
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
// 注销
key.cancel();
channel.close();
selector.close(); // 关闭 Selector 释放资源
Channel 与 Buffer 的交互
基本概念
-
Channel(通道)
- 类似于传统 I/O 中的流(Stream),但支持双向数据传输(读/写)。
- 常见实现:
FileChannel
、SocketChannel
、ServerSocketChannel
、DatagramChannel
。
-
Buffer(缓冲区)
- 本质是一块内存区域,用于临时存储数据。
- 核心属性:
capacity
(容量)、position
(当前位置)、limit
(读写上限)、mark
(标记位)。 - 常见类型:
ByteBuffer
、CharBuffer
、IntBuffer
等。
交互流程
-
数据从 Channel 读取到 Buffer
FileChannel channel = FileChannel.open(Paths.get("file.txt")); ByteBuffer buffer = ByteBuffer.allocate(1024); int bytesRead = channel.read(buffer); // 数据写入Buffer
-
数据从 Buffer 写入到 Channel
buffer.flip(); // 切换为读模式(position=0, limit=原position) channel.write(buffer); // 数据从Buffer读出到Channel
关键操作
-
Buffer 模式切换
flip()
:写模式 → 读模式(limit=position
,position=0
)。clear()
:读模式 → 写模式(position=0
,limit=capacity
)。rewind()
:重置position=0
,可重复读取数据。
-
直接缓冲区(Direct Buffer)
- 通过
ByteBuffer.allocateDirect()
创建,直接使用操作系统内存,减少拷贝开销,适合大文件或高频 I/O。
- 通过
注意事项
-
Buffer 需初始化容量
- 分配过小会导致多次 I/O 操作,过大浪费内存。
-
正确处理模式切换
- 未调用
flip()
直接写入 Channel 会导致数据错误。
- 未调用
-
资源释放
- Channel 需显式调用
close()
或通过 try-with-resources 管理。
- Channel 需显式调用
完整示例
try (FileChannel srcChannel = FileChannel.open(Paths.get("src.txt"));
FileChannel destChannel = FileChannel.open(Paths.get("dest.txt"),
StandardOpenOption.CREATE,
StandardOpenOption.WRITE)) {
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while (srcChannel.read(buffer) != -1) {
buffer.flip();
destChannel.write(buffer);
buffer.clear(); // 或 compact()(保留未读数据)
}
}
四、Buffer 详解
Buffer 的核心作用
概念定义
Buffer(缓冲区)是 Java NIO 中用于临时存储数据的内存块,本质上是一块可以读写数据的线性内存空间。它充当数据源(如文件、网络)与应用程序之间的中间媒介,通过减少直接 I/O 操作次数来提升性能。
核心功能
-
数据暂存
- 读写数据时,先将数据加载到 Buffer,再由程序处理,避免频繁直接操作物理设备(如磁盘、网卡)。
- 示例:读取文件时,一次性从磁盘加载 1KB 到 Buffer,程序多次从 Buffer 读取,而非每次访问磁盘。
-
读写分离
- 通过
position
、limit
、capacity
等指针属性,实现读写模式切换(flip()
方法)和高效数据操作。
- 通过
-
批量传输
- 支持一次性读写多个字节/字符(如
put(byte[])
、get(byte[])
),减少系统调用开销。
- 支持一次性读写多个字节/字符(如
使用场景
-
文件 I/O
try (FileChannel channel = FileChannel.open(Paths.get("test.txt"))) { ByteBuffer buffer = ByteBuffer.allocate(1024); while (channel.read(buffer) != -1) { buffer.flip(); // 切换为读模式 // 处理buffer中的数据 buffer.clear(); // 重置buffer } }
-
网络通信
- SocketChannel 通过 Buffer 收发数据,避免逐字节处理。
-
高性能数据处理
- 结合 DirectByteBuffer(堆外内存)减少 JVM 堆与操作系统间的数据拷贝。
注意事项
-
指针管理
- 读写后需正确调用
flip()
、rewind()
或clear()
,否则可能导致数据错乱或溢出。
- 读写后需正确调用
-
内存分配
- 堆内 Buffer(
allocate()
)受 GC 管理,但存在拷贝开销;堆外 Buffer(allocateDirect()
)性能更高,但需手动释放。
- 堆内 Buffer(
-
线程安全
- Buffer 非线程安全,多线程访问需同步。
常见误区
-
误区:Buffer 越大性能越好。
正解:过大的 Buffer 会占用过多内存,需根据实际数据量权衡(通常 1KB~8KB)。 -
误区:
clear()
会清空数据。
正解:clear()
仅重置指针,数据仍存在内存中,但会被后续写入覆盖。
Buffer 的基本结构
Buffer 是 NIO 中用于高效读写数据的核心组件,本质上是一个固定大小的线性内存块。其内部通过三个关键属性(position
、limit
、capacity
)协同工作,控制数据的读写边界。
核心属性
-
Capacity(容量)
- Buffer 的最大数据容量,创建时确定且不可变。
- 例如:
ByteBuffer.allocate(1024)
的capacity
为 1024 字节。
-
Position(位置指针)
- 下一个读写操作的位置,初始为 0。
- 写模式:每写入一个数据,
position
自增。 - 读模式:调用
flip()
后position
重置为 0,读取时自增。
-
Limit(读写边界)
- 写模式:
limit = capacity
,表示最多能写入的数据量。 - 读模式:
limit
为写模式结束时的position
值,表示可读取的数据上限。
- 写模式:
状态转换示例
ByteBuffer buffer = ByteBuffer.allocate(10); // capacity=10, position=0, limit=10
// 写入数据
buffer.put((byte) 'A'); // position=1, limit=10
buffer.put((byte) 'B'); // position=2, limit=10
// 切换为读模式
buffer.flip(); // position=0, limit=2 (上次写入的结束位置)
// 读取数据
byte a = buffer.get(); // position=1, limit=2
byte b = buffer.get(); // position=2, limit=2
// 重置为写模式
buffer.clear(); // position=0, limit=10 (恢复初始状态)
关键操作与属性变化
操作 | Position | Limit | 用途 |
---|---|---|---|
allocate(n) | 0 | capacity = n | 创建空 Buffer |
put(data) | 自增(+1/单位) | 不变 | 写入数据 |
flip() | 0 | 原 position | 写→读模式切换 |
get() | 自增 | 不变 | 读取数据 |
clear() | 0 | capacity | 清空 Buffer(数据未擦除) |
注意事项
- 读写切换必须调用
flip()
或rewind()
,否则position
越界可能抛出BufferUnderflowException
。 clear()
不会清空数据,仅重置指针,后续写入会覆盖原有数据。- 直接操作指针需谨慎,错误的
position/limit
设置会导致数据错乱。
常见 Buffer 类型
Java NIO 中的 Buffer
是一个抽象类,主要用于与通道(Channel
)进行数据交互。以下是常见的 Buffer
子类及其用途:
ByteBuffer
- 定义:用于存储字节数据,是 NIO 中最常用的缓冲区类型。
- 特点:
- 支持直接内存(
allocateDirect
),减少 JVM 堆与操作系统内存的拷贝。 - 提供
put()
/get()
方法操作字节数据。
- 支持直接内存(
- 使用场景:文件 I/O、网络通信(如 Socket 数据传输)。
- 示例代码:
ByteBuffer buffer = ByteBuffer.allocate(1024); // 分配堆内存 buffer.put((byte) 1); // 写入数据 buffer.flip(); // 切换为读模式 byte b = buffer.get(); // 读取数据
CharBuffer
- 定义:存储字符数据,基于
CharSequence
实现。 - 特点:
- 支持字符编码/解码(通过
Charset
)。 - 提供
append()
/read()
等字符操作。
- 支持字符编码/解码(通过
- 使用场景:文本处理(如文件编码转换)。
- 示例代码:
CharBuffer charBuffer = CharBuffer.allocate(100); charBuffer.put("Hello"); charBuffer.flip(); while (charBuffer.hasRemaining()) { System.out.print(charBuffer.get()); }
IntBuffer / LongBuffer / FloatBuffer / DoubleBuffer
- 定义:分别用于存储
int
、long
、float
、double
类型数据。 - 特点:
- 提供类型化数据操作(如
putInt()
/getFloat()
)。 - 避免手动处理字节对齐问题。
- 提供类型化数据操作(如
- 使用场景:数值计算、二进制协议解析。
- 示例代码(以
IntBuffer
为例):IntBuffer intBuffer = IntBuffer.allocate(10); intBuffer.put(42); intBuffer.flip(); int value = intBuffer.get();
ShortBuffer
- 定义:存储
short
类型数据。 - 特点:与
IntBuffer
类似,但用于 16 位短整型。 - 使用场景:音频处理、低层网络协议(如 TCP 端口号)。
注意事项
- 模式切换:写入后必须调用
flip()
切换为读模式,反之用clear()
/compact()
。 - 直接缓冲区:
ByteBuffer.allocateDirect()
分配的内存不受 GC 管理,需谨慎使用。 - 类型匹配:避免用
ByteBuffer.getInt()
读取非对齐的字节数据,可能抛出BufferUnderflowException
。
常见误区
- 误用
array()
:只有非直接缓冲区(堆内存)支持array()
方法,直接缓冲区调用会抛UnsupportedOperationException
。 - 忽略
limit
和position
:操作数据时需注意这两个指针的位置,否则可能导致数据读写错误。
Buffer 的读写操作
基本概念
Buffer 是 NIO 中用于数据存储的核心对象,本质是一个固定大小的内存块,提供对数据的读写操作。常见的 Buffer 类型包括 ByteBuffer
、CharBuffer
、IntBuffer
等。
核心属性
- capacity:Buffer 的容量,创建时固定。
- position:当前读写位置,初始为 0。
- limit:可操作数据的上界,初始等于 capacity。
- mark:标记位置,可通过
reset()
恢复。
读写流程
-
写入数据:
- 通过
put()
方法写入数据,position
随之移动。 - 写入完成后调用
flip()
,切换为读模式:limit
设置为当前position
(表示可读数据量)。position
重置为 0。
- 通过
-
读取数据:
- 通过
get()
方法读取数据,position
随之移动。 - 读取完成后可调用
clear()
或compact()
切换回写模式:clear()
:重置position=0
,limit=capacity
(丢弃已读数据)。compact()
:保留未读数据,将其移动到 Buffer 头部。
- 通过
示例代码
// 创建 ByteBuffer(容量为 10)
ByteBuffer buffer = ByteBuffer.allocate(10);
// 写入数据
buffer.put((byte) 'A');
buffer.put((byte) 'B');
buffer.flip(); // 切换为读模式
// 读取数据
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get()); // 输出: AB
}
// 清空 Buffer(切换回写模式)
buffer.clear();
注意事项
- 模式切换:读写操作前必须通过
flip()
/clear()
明确模式。 - 越界检查:手动操作
position
/limit
时需避免越界。 - 直接缓冲区:
allocateDirect()
创建的 Buffer 性能更高,但分配成本较大。
常用方法
方法 | 作用 |
---|---|
put() / get() | 读写单个数据 |
put(byte[]) | 批量写入数据 |
flip() | 写模式 → 读模式 |
clear() | 读模式 → 写模式(清空) |
compact() | 保留未读数据并切换为写模式 |
rewind() | 重置 position=0 (可重复读) |
Buffer 的清理与压缩
概念定义
Buffer 的清理(clear)与压缩(compact)是 NIO 中用于重置或优化缓冲区状态的两种操作:
- clear():将缓冲区重置为初始状态(position=0, limit=capacity),但不实际清除数据。
- compact():将未读取的数据(position到limit之间)复制到缓冲区头部,并重置position为剩余数据末尾,limit=capacity。
使用场景
- clear():当需要完全重用缓冲区时(如新的读写循环)。
- compact():当缓冲区中残留部分未读数据,但需要继续写入时(如非阻塞网络通信中的半包处理)。
示例代码
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 模拟写入数据
buffer.put("Hello, World!".getBytes());
buffer.flip(); // 切换为读模式
// 读取部分数据
byte[] partial = new byte[5];
buffer.get(partial);
System.out.println(new String(partial)); // 输出 "Hello"
// 清理缓冲区(完全重置)
buffer.clear(); // position=0, limit=1024
// 压缩缓冲区(保留未读数据)
buffer.compact(); // " World!" 会被移动到缓冲区头部
注意事项
- clear() 不会擦除数据:只是重置指针,原有数据可能被新数据覆盖。
- compact() 有内存复制开销:频繁调用可能影响性能。
- 读写模式切换:操作前需确保缓冲区处于正确模式(通常compact在写模式前调用)。
- limit 变化:clear()后limit=capacity,compact()后limit可能小于capacity。
常见误区
- 误认为clear()会清空数据内容(实际仅重置指针)。
- 在残留未处理数据时错误使用clear()导致数据丢失。
- 未检查剩余空间直接写入,导致BufferOverflowException。
五、多路复用实现原理
操作系统底层支持(select/poll/epoll)
概念定义
select、poll、epoll 是操作系统提供的 I/O 多路复用机制,用于高效管理多个文件描述符(如套接字)。它们允许单个线程监视多个 I/O 事件,避免多线程/进程的开销。
select
原理
- 通过
fd_set
位图管理文件描述符集合。 - 每次调用需将整个集合从用户态拷贝到内核态。
- 内核线性扫描所有描述符,返回就绪事件数量。
限制
- 文件描述符数量受限(通常 1024)。
- 每次调用需重置
fd_set
,重复拷贝开销大。 - O(n) 时间复杂度扫描。
poll
改进
- 使用
pollfd
结构体数组替代fd_set
,突破数量限制。 - 无需每次重置整个集合。
保留问题
- 仍需要全量扫描所有描述符。
- 大量空闲连接时性能下降。
epoll(Linux 特有)
核心优化
- 事件驱动:通过
epoll_ctl
注册兴趣事件,内核维护红黑树存储。 - 就绪列表:内核通过回调机制将就绪事件加入链表,
epoll_wait
直接获取。 - 内存共享:使用
mmap
避免用户态与内核态数据拷贝。
关键 API
int epoll_create(int size); // 创建 epoll 实例
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // 管理事件
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); // 等待事件
触发模式
- 水平触发(LT):只要文件描述符就绪,就会持续通知(默认模式)。
- 边缘触发(ET):仅在状态变化时通知一次,需非阻塞读取。
性能对比
特性 | select | poll | epoll |
---|---|---|---|
时间复杂度 | O(n) | O(n) | O(1) |
最大连接数 | 1024 | 无限制 | 无限制 |
内存拷贝 | 每次全量拷贝 | 每次全量拷贝 | 首次注册后无拷贝 |
适用场景 | 跨平台小连接 | 大连接但低活跃 | 大连接高活跃 |
使用场景
- select/poll:跨平台需求或连接数极少的场景。
- epoll:Linux 高并发网络服务(如 Nginx、Redis)。
注意事项
- ET 模式必须使用非阻塞 IO:避免因未读完数据导致事件丢失。
- epoll 惊群问题:多进程监听同一 epoll 实例时可能被全部唤醒,需通过
EPOLLEXCLUSIVE
解决。 - 监控描述符类型:常规文件描述符(如磁盘文件)不支持异步通知。
Java 示例(Selector 底层实现)
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT); // 注册 accept 事件
while (true) {
selector.select(); // 底层调用 epoll_wait
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (key.isAcceptable()) {
// 处理新连接
}
if (key.isReadable()) {
// 处理读事件
}
}
keys.clear();
}
事件驱动机制
概念定义
事件驱动机制是一种编程范式,程序的执行流程由外部事件(如用户输入、网络消息、定时器等)触发,而非传统的顺序执行。核心组件包括:
- 事件源:产生事件的对象(如Socket、按钮)
- 事件监听器:处理事件的回调接口
- 事件循环:持续检测并分发事件的调度中心
在NIO中的应用
Java NIO通过Selector
实现事件驱动:
- 将多个
Channel
注册到Selector
- 通过
select()
阻塞等待事件(OP_READ/OP_WRITE等) - 事件触发后通过
selectedKeys()
获取就绪的Channel集合
与传统阻塞IO对比
特性 | 事件驱动 | 阻塞IO |
---|---|---|
线程模型 | 单线程处理多连接 | 一连接一线程 |
资源消耗 | 低(减少线程切换) | 高 |
响应速度 | 延迟敏感型更优 | 稳定但吞吐量受限 |
代码示例
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); // 阻塞等待事件
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
if (key.isAcceptable()) {
// 处理新连接
} else if (key.isReadable()) {
// 处理读事件
}
iter.remove();
}
}
注意事项
- 事件去重:
selectedKeys()
返回的集合需要手动移除已处理的Key - 线程安全:
Selector
本身线程安全,但注册操作需要同步 - 空轮询BUG:某些JDK版本可能出现
select()
不阻塞,需通过计数器检测
适用场景
- 高并发网络服务(如Web服务器)
- 需要处理大量长连接的场景
- 延迟敏感型应用(如实时通信系统)
就绪选择过程详解
概念定义
就绪选择(Ready Selection)是NIO多路复用机制中的核心过程,指通过系统调用(如select
/poll
/epoll
)批量检测多个通道的I/O就绪状态,避免为每个通道单独阻塞等待。其本质是事件驱动的I/O通知机制。
关键步骤
-
注册兴趣事件
将需要监听的通道(如SocketChannel
)注册到Selector
,并指定关注的事件类型(如OP_READ
/OP_WRITE
)。channel.configureBlocking(false); selector.register(channel, SelectionKey.OP_READ);
-
阻塞式轮询
调用selector.select()
进入阻塞,直到至少有一个注册的通道就绪。底层通过操作系统级调用实现高效监控。 -
获取就绪集合
通过selector.selectedKeys()
获取已就绪的SelectionKey
集合,每个Key
包含就绪的通道和事件类型。 -
事件处理
遍历就绪集合,根据事件类型执行对应I/O操作(如读取数据或写入缓冲区)。
底层实现差异
模型 | 实现方式 | 特点 |
---|---|---|
select | 遍历fd集合(位数组) | 有最大fd限制(1024) |
poll | 链表存储fd | 无数量限制但性能线性下降 |
epoll | 回调机制+红黑树存储 | O(1)时间复杂度,支持水平触发 |
注意事项
-
避免空轮询
JDK的epoll
实现可能存在空转BUG(如无事件时select()
立即返回),需通过selector.selectNow()
或超时参数规避。 -
线程安全
Selector
本身线程安全,但selectedKeys()
返回的集合不支持并发操作,需同步处理。 -
事件清除
处理完SelectionKey
后需手动从集合中移除,否则下次select()
会重复通知:Iterator<SelectionKey> it = selectedKeys.iterator(); while (it.hasNext()) { SelectionKey key = it.next(); it.remove(); // 关键步骤! // 处理事件... }
性能优化点
- 对高吞吐场景优先使用
epoll
(Linux默认) - 合并短时连续的写事件(通过
OP_WRITE
触发) - 使用
DirectByteBuffer
减少内存拷贝
水平触发(Level-Triggered)与边缘触发(Edge-Triggered)
概念定义
-
水平触发(LT)
当文件描述符(fd)就绪(可读/可写)时,只要状态未变化,会持续通知应用程序。例如:- 若缓冲区有数据未读完,下次调用
epoll_wait()
仍会返回该 fd。 - 默认模式,行为类似
select()
/poll()
。
- 若缓冲区有数据未读完,下次调用
-
边缘触发(ET)
仅在 fd 状态变化时(如从不可读到可读)触发一次通知,后续不再提醒,除非有新事件发生。- 需一次性处理完数据,否则可能丢失事件。
- 必须搭配非阻塞 I/O 使用。
使用场景
模式 | 适用场景 | 不适用场景 |
---|---|---|
LT | 简单场景,代码容错性高 | 高频事件(可能重复触发) |
ET | 高性能场景(如百万连接) | 未正确处理导致事件丢失 |
关键区别
特性 | LT | ET |
---|---|---|
通知频率 | 状态持续则重复通知 | 仅状态变化时通知一次 |
代码复杂度 | 低(可多次处理) | 高(需一次处理完) |
性能 | 可能多次触发系统调用 | 减少无效调用,性能更高 |
示例代码(ET 模式注意事项)
// 非阻塞读取(ET 必须使用)
try (ServerSocketChannel serverChannel = ServerSocketChannel.open()) {
serverChannel.configureBlocking(false);
serverChannel.bind(new InetSocketAddress(8080));
Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey key = keys.next();
keys.remove();
if (key.isAcceptable()) {
// 处理连接...
} else if (key.isReadable()) {
// ET 模式下必须循环读取到无数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
SocketChannel channel = (SocketChannel) key.channel();
while (channel.read(buffer) > 0) { // 非阻塞读取
buffer.flip();
// 处理数据...
buffer.clear();
}
}
}
}
}
常见误区
-
ET 未使用非阻塞 I/O
会导致最后一次读取阻塞线程。 -
ET 未完全处理数据
剩余数据不会再次触发事件。 -
LT 忽略 EPOLLOUT 事件
写缓冲区满时需监听EPOLLOUT
,否则可能死锁。
多线程与多路复用的结合
概念定义
多线程与多路复用的结合是指在高并发网络编程中,利用多线程处理多个 I/O 事件,同时通过多路复用技术(如 Selector
)高效监听多个通道(Channel)的就绪状态,从而提升系统吞吐量。核心目标是:
- 多路复用:单线程监听多个通道的 I/O 事件(如读、写、连接),避免为每个连接创建线程。
- 多线程:将就绪的 I/O 事件分发给线程池处理,充分利用多核 CPU。
典型架构
// 伪代码示例
Selector selector = Selector.open(); // 多路复用核心
ThreadPoolExecutor workerPool = new ThreadPoolExecutor(...); // 工作线程池
while (true) {
selector.select(); // 阻塞等待就绪事件
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (key.isReadable()) {
workerPool.execute(() -> handleRead(key)); // 交给线程池处理
}
// 其他事件处理...
}
keys.clear();
}
使用场景
- 高并发服务器:如 Web 服务器(Tomcat NIO 模式)、即时通讯系统。
- 延迟敏感型应用:需要快速响应大量客户端请求,但每个请求的计算不复杂。
- 资源受限环境:避免为每个连接创建线程(减少内存/上下文切换开销)。
关键实现方式
- 主从 Reactor 模式(Netty 采用):
- 主线程(单线程):负责监听
Accept
事件,建立连接后分发给子线程。 - 子线程组:每个子线程独立运行
Selector
,处理分配的连接上的 I/O 事件。
- 主线程(单线程):负责监听
- 线程池分发:
- 单
Selector
线程检测事件,通过线程池异步处理就绪的 Channel。
- 单
注意事项
- 线程安全:
- Channel 的操作(如
read/write
)需确保线程安全,避免多线程同时操作同一 Channel。 - 可通过
SelectionKey.attach()
绑定线程专属的缓冲区。
- Channel 的操作(如
- 避免阻塞:
- 工作线程中不要执行长时间阻塞操作(如同步数据库调用),否则会拖慢整个事件循环。
- 负载均衡:
- 子线程间的连接分配需均匀,避免某些线程过载(如采用轮询或哈希分配)。
示例代码(简化版主从 Reactor)
// 主线程处理 Accept
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(8080));
ssc.configureBlocking(false);
Selector masterSelector = Selector.open();
ssc.register(masterSelector, SelectionKey.OP_ACCEPT);
// 子线程组(每个线程一个 Selector)
Selector[] workerSelectors = new Selector[4];
for (int i = 0; i < workerSelectors.length; i++) {
workerSelectors[i] = Selector.open();
new Thread(() -> {
while (true) {
workerSelectors[i].select();
// 处理 I/O 事件...
}
}).start();
}
// 主循环:分发新连接到子线程
while (true) {
masterSelector.select();
SelectionKey key = masterSelector.selectedKeys().iterator().next();
if (key.isAcceptable()) {
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
// 轮询分配给子线程
workerSelectors[nextWorkerIndex].register(sc, SelectionKey.OP_READ);
}
}
六、NIO 多路复用编程实践
服务端实现步骤
1. 创建 ServerSocketChannel
- 使用
ServerSocketChannel.open()
创建通道 - 配置为非阻塞模式:
serverSocketChannel.configureBlocking(false)
- 绑定监听端口:
serverSocketChannel.bind(new InetSocketAddress(port))
2. 创建 Selector
- 通过
Selector.open()
创建选择器 - 将 ServerSocketChannel 注册到 Selector,监听 ACCEPT 事件:
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
3. 事件循环处理
while (true) {
// 阻塞等待就绪的Channel
int readyChannels = selector.select();
if (readyChannels == 0) continue;
// 获取就绪的SelectionKey集合
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// 处理ACCEPT事件
} else if (key.isReadable()) {
// 处理READ事件
} else if (key.isWritable()) {
// 处理WRITE事件
}
keyIterator.remove(); // 必须手动移除
}
}
4. 处理ACCEPT事件
ServerSocketChannel server = (ServerSocketChannel)key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
5. 处理READ事件
SocketChannel client = (SocketChannel)key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = client.read(buffer);
if (bytesRead == -1) { // 连接关闭
key.cancel();
client.close();
} else {
// 处理读取到的数据
buffer.flip();
// ...数据处理逻辑...
}
6. 处理WRITE事件
SocketChannel client = (SocketChannel)key.channel();
ByteBuffer buffer = ByteBuffer.wrap("响应数据".getBytes());
while (buffer.hasRemaining()) {
client.write(buffer);
}
// 写完后取消监听WRITE事件,避免一直触发
key.interestOps(SelectionKey.OP_READ);
7. 资源清理
- 在程序退出时关闭Selector和Channel:
selector.close();
serverSocketChannel.close();
注意事项
- 每次迭代必须调用
keyIterator.remove()
- 写操作应该只在需要时才注册WRITE事件
- 正确处理连接关闭的情况
- 避免在事件处理中进行长时间阻塞操作
客户端实现步骤
1. 创建Selector
Selector selector = Selector.open();
2. 创建SocketChannel并配置
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false); // 必须设置为非阻塞模式
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
3. 注册Channel到Selector
// 首次注册时关注CONNECT事件
socketChannel.register(selector, SelectionKey.OP_CONNECT);
4. 事件循环处理
while (true) {
selector.select(); // 阻塞等待就绪事件
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectedKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove(); // 必须移除已处理的key
if (key.isConnectable()) {
handleConnect(key);
} else if (key.isReadable()) {
handleRead(key);
}
}
}
5. 连接就绪处理
private void handleConnect(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
if (channel.finishConnect()) {
// 连接成功后注册读事件
channel.register(key.selector(), SelectionKey.OP_READ);
// 可以开始发送数据
ByteBuffer buffer = ByteBuffer.wrap("Hello Server".getBytes());
channel.write(buffer);
}
}
6. 读就绪处理
private void handleRead(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = channel.read(buffer);
if (read > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
System.out.println("Received: " + new String(bytes));
} else if (read < 0) {
channel.close(); // 连接已关闭
}
}
关键注意事项
- 必须设置非阻塞模式(
configureBlocking(false)
) - 每次迭代必须调用
iter.remove()
清除已处理的key - 连接建立后需要手动调用
finishConnect()
- 读写操作要注意处理返回值(可能只传输了部分数据)
- 正确处理连接关闭的情况(read返回-1)
处理连接事件
概念定义
在NIO多路复用中,处理连接事件是指当服务器套接字通道(ServerSocketChannel)准备好接受新客户端连接时,Selector会触发OP_ACCEPT事件。这是建立新连接的关键步骤。
使用场景
- 服务器端程序初始化后监听端口
- 客户端发起连接请求时
- 需要高效处理大量并发连接的场景
核心处理流程
// 当Selector检测到OP_ACCEPT事件时
if (key.isAcceptable()) {
// 1. 接受客户端连接
SocketChannel clientChannel = serverSocketChannel.accept();
// 2. 配置为非阻塞模式
clientChannel.configureBlocking(false);
// 3. 注册到Selector,监听读写事件
clientChannel.register(selector, SelectionKey.OP_READ);
// 4. 记录新连接(可选)
System.out.println("新客户端连接: " + clientChannel.getRemoteAddress());
}
注意事项
- 非阻塞设置:必须将新接受的SocketChannel设置为非阻塞模式
- 资源管理:及时关闭不需要的连接,避免资源泄漏
- 事件注册:通常新连接会立即注册OP_READ事件准备接收数据
- 性能优化:在高并发场景下,accept()应快速处理,避免阻塞事件循环
常见误区
- 忘记设置非阻塞模式,导致后续操作阻塞
- 未正确处理accept()可能返回null的情况
- 在同一个通道上多次调用accept()而不检查返回值
- 未考虑连接数限制,可能导致文件描述符耗尽
完整示例片段
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);
Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (key.isAcceptable()) {
handleAccept(key);
}
// 处理其他事件...
}
keys.clear();
}
void handleAccept(SelectionKey key) throws IOException {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
System.out.println("Accepted connection from: " + client.getRemoteAddress());
}
处理读写事件
概念定义
在NIO多路复用中,处理读写事件是指当Selector检测到某个通道(Channel)准备好进行读或写操作时,应用程序如何响应这些事件。通过Selector的select()方法可以获取就绪的通道集合,然后遍历这些通道并执行相应的I/O操作。
使用场景
- 读事件处理:当通道中有数据可读时触发,常用于接收客户端请求或读取文件数据。
- 写事件处理:当通道可写时触发,常用于发送响应数据或写入文件。
- 高性能网络通信:如聊天服务器、文件传输等需要高效处理大量并发连接的场景。
关键步骤
- 获取就绪事件集合:通过
Selector.selectedKeys()
获取已就绪的事件集合。 - 遍历事件集合:检查每个事件的就绪状态(如
SelectionKey.OP_READ
或SelectionKey.OP_WRITE
)。 - 执行I/O操作:
- 读事件:调用
channel.read(buffer)
读取数据。 - 写事件:调用
channel.write(buffer)
写入数据。
- 读事件:调用
- 移除已处理事件:处理完成后需手动从集合中移除事件,避免重复处理。
示例代码
Selector selector = Selector.open();
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
while (true) {
selector.select();
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectedKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
if (key.isReadable()) {
// 处理读事件
SocketChannel ch = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = ch.read(buffer);
if (bytesRead > 0) {
buffer.flip();
System.out.println("Received: " + new String(buffer.array(), 0, bytesRead));
}
} else if (key.isWritable()) {
// 处理写事件
SocketChannel ch = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.wrap("Hello, Client!".getBytes());
ch.write(buffer);
}
iter.remove(); // 移除已处理的事件
}
}
注意事项
- 事件移除:处理完事件后必须调用
iterator.remove()
,否则下次循环会重复处理。 - 缓冲区管理:读写操作需正确操作缓冲区(如
flip()
和clear()
)。 - 资源释放:通道或Selector使用完毕后需关闭,避免资源泄漏。
- 线程安全:多线程环境下需注意Selector和通道的线程安全性。
异常处理机制
概念定义
异常处理机制是 Java 中用于处理程序运行时可能出现的错误或异常情况的一种机制。它允许程序在遇到异常时,能够优雅地处理错误,而不是直接崩溃。Java 中的异常分为两大类:
- Checked Exception(受检异常):编译器强制要求处理的异常,如
IOException
。 - Unchecked Exception(非受检异常):运行时异常,如
NullPointerException
或ArrayIndexOutOfBoundsException
。
核心关键字
try
:用于包裹可能抛出异常的代码块。catch
:捕获并处理特定类型的异常。finally
:无论是否发生异常,都会执行的代码块,通常用于资源释放。throw
:手动抛出异常。throws
:声明方法可能抛出的异常。
使用场景
- 文件操作:处理
IOException
。 - 网络通信:处理连接超时或数据读取异常。
- 数据库操作:处理
SQLException
。 - 参数校验:通过抛出
IllegalArgumentException
提示调用方。
示例代码
try {
// 可能抛出异常的代码
FileInputStream file = new FileInputStream("test.txt");
int data = file.read();
} catch (FileNotFoundException e) {
System.out.println("文件未找到: " + e.getMessage());
} catch (IOException e) {
System.out.println("读取文件出错: " + e.getMessage());
} finally {
System.out.println("无论是否异常,都会执行");
}
常见误区
- 捕获过于宽泛的异常:如直接捕获
Exception
,会掩盖具体问题。 - 忽略异常:空的
catch
块会导致问题被隐藏。 - 过度使用异常:异常处理应针对异常情况,而非正常逻辑控制。
最佳实践
- 具体化异常类型:优先捕获具体的异常类。
- 资源管理:使用
try-with-resources
自动关闭资源。try (FileInputStream file = new FileInputStream("test.txt")) { // 自动关闭文件流 }
- 自定义异常:通过继承
Exception
或RuntimeException
定义业务异常。
注意事项
- 性能开销:异常处理比普通代码慢,避免在频繁执行的代码中使用异常控制流程。
- 异常链:通过
initCause()
或构造方法传递原始异常,便于调试。 - 日志记录:在
catch
块中记录异常详细信息,但避免重复记录。
七、性能优化与注意事项
Selector 空轮询问题
概念定义
Selector 空轮询问题是指在使用 Java NIO 的 Selector 进行多路复用时,Selector 的 select()
方法在没有就绪的 Channel 时仍然立即返回,导致 CPU 空转的现象。这通常是由于底层操作系统的 epoll 实现缺陷(在 Linux 内核某些版本中)触发的。
原因分析
- epoll 内核 Bug:某些 Linux 内核版本(如 2.6.x)的 epoll 实现存在缺陷,当被监控的文件描述符(fd)遇到特定事件(如断开连接)时,epoll 会错误地唤醒
select()
,但实际上没有就绪事件。 - Selector 未正确重建:在 JDK 的实现中,Selector 依赖底层操作系统的多路复用机制(如 epoll),若未及时处理异常或重建 Selector,会导致空轮询持续发生。
影响
- CPU 占用率飙升:空轮询会导致线程不断循环调用
select()
,占用大量 CPU 资源。 - 性能下降:空轮询线程可能阻塞其他正常任务的执行。
解决方案
1. 检测并重建 Selector
通过记录 select()
的调用次数和耗时,判断是否发生空轮询。若短时间内 select()
返回次数过多(如连续 10 次返回但无事件),则重建 Selector。
示例代码:
// 记录空轮询次数
int emptyPollCount = 0;
long startTime = System.currentTimeMillis();
while (true) {
int readyChannels = selector.select(1000); // 设置超时 1s
if (readyChannels == 0) {
emptyPollCount++;
// 检测 1s 内是否发生空轮询(如超过 10 次)
if (System.currentTimeMillis() - startTime < 1000 && emptyPollCount > 10) {
// 重建 Selector
rebuildSelector();
emptyPollCount = 0;
startTime = System.currentTimeMillis();
}
} else {
emptyPollCount = 0;
startTime = System.currentTimeMillis();
// 处理就绪事件
}
}
2. 升级 JDK 或 Linux 内核
- JDK 1.7+ 对空轮询问题进行了优化(如
sun.nio.ch.bugLevel
机制)。 - 升级 Linux 内核到较新版本(如 4.x+),修复 epoll 缺陷。
3. 设置超时时间
避免使用无参 select()
,始终设置合理的超时时间(如 select(1000)
),减少空轮询的负面影响。
注意事项
- 重建 Selector 的代价:重建 Selector 需要重新注册所有 Channel,可能引起短暂性能下降。
- 兼容性:不同操作系统和 JDK 版本的行为可能不同,需针对性测试。
- 监控工具:可通过
jstack
或top
监控线程 CPU 使用情况,及时发现空轮询。
NIO 多路复用:处理大量连接时的优化
核心思想
NIO 多路复用通过单线程管理多个连接,解决传统 BIO 中"一线程一连接"的资源浪费问题。核心组件:
- Selector:监听多个 Channel 的事件(读/写/连接)
- 非阻塞 Channel:连接注册到 Selector 后无需阻塞等待
关键优化点
1. 资源占用对比
模型 | 线程数 | 内存消耗 | 上下文切换 |
---|---|---|---|
BIO | 1:1 连接 | 高 | 频繁 |
多路复用 | 1:N 连接 | 低 | 极少 |
2. 事件驱动机制
// 典型事件处理循环
while (selector.select() > 0) {
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
if (key.isReadable()) {
handleRead(key);
} else if (key.isWritable()) {
handleWrite(key);
}
it.remove(); // 必须移除已处理事件
}
}
3. 水平触发 vs 边缘触发
- 水平触发(默认):事件未处理会持续通知
- 边缘触发:仅状态变化时通知一次(需一次处理完数据)
性能瓶颈解决方案
- 空轮询问题:
// JDK修复方案:重建Selector if (selector.select() == 0 && System.currentTimeMillis() - lastSelectTime > timeout) { rebuildSelector(); }
- 事件积压处理:
- 分离读/写线程池
- 使用
LinkedBlockingQueue
缓冲任务
最佳实践
- Channel配置优化:
channel.configureBlocking(false); channel.socket().setTcpNoDelay(true); // 禁用Nagle算法 channel.socket().setReuseAddress(true);
- Selector使用规范:
- 避免在
select()
时修改注册的Channel - 及时取消已关闭的
SelectionKey
- 避免在
适用场景
- 长连接服务(如IM、推送系统)
- 连接数 > 1000 的高并发场景
- 延迟敏感型应用(如游戏服务器)
注意事项
- 单个Selector处理连接数建议不超过10,000
- 写操作需自行处理写半包问题
- 避免在事件处理中进行耗时操作
内存管理概述
内存管理是Java技术体系中至关重要的部分,主要涉及对象的创建、使用和销毁。良好的内存管理可以显著提升应用性能,减少内存泄漏和垃圾回收(GC)的开销。
堆内存与栈内存
- 堆内存:存储对象实例和数组,由垃圾回收器管理。
- 栈内存:存储基本数据类型和对象引用,方法调用时分配,方法结束时自动释放。
内存管理最佳实践
1. 对象创建与复用
- 避免频繁创建对象:尤其在循环或高频调用方法中,尽量复用对象。
- 使用对象池:对于创建成本高的对象(如数据库连接、线程等),使用对象池(如Apache Commons Pool)。
示例代码:
// 避免在循环中创建对象
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i); // 复用StringBuilder对象
}
2. 及时释放资源
- 显式关闭资源:使用
try-with-resources
确保资源(如文件流、数据库连接)及时关闭。 - 避免内存泄漏:清除无用的对象引用,尤其是集合中的对象。
示例代码:
try (FileInputStream fis = new FileInputStream("file.txt")) {
// 使用资源
} // 自动关闭
3. 合理设置堆大小
- 初始堆大小(-Xms):设置为应用稳定运行时的最小需求。
- 最大堆大小(-Xmx):根据应用峰值需求设置,避免频繁GC。
4. 优化数据结构
- 选择合适的数据结构:如
ArrayList
vsLinkedList
,HashMap
vsTreeMap
。 - 预分配集合大小:避免集合动态扩容带来的性能开销。
示例代码:
List<String> list = new ArrayList<>(1000); // 预分配大小
5. 监控与分析
- 使用工具监控内存:如JVisualVM、MAT(Memory Analyzer Tool)。
- 分析内存泄漏:通过堆转储(Heap Dump)定位问题。
常见误区
- 过度依赖GC:认为GC会自动处理所有内存问题,忽视显式资源释放。
- 静态集合滥用:静态集合长期持有对象引用,导致内存泄漏。
- 未优化的大对象:如大数组或缓存未合理管理,引发频繁GC。
注意事项
- 避免finalize方法:
finalize()
执行不确定,可能引发性能问题。 - 谨慎使用软引用/弱引用:需根据场景选择,避免误用。
线程模型选择建议
1. 阻塞I/O模型(BIO)
- 适用场景:连接数较少(<1000)、逻辑简单的同步处理场景。
- 特点:每个连接对应一个线程,编程简单但资源消耗大。
- 示例代码:
ServerSocket server = new ServerSocket(8080);
while(true) {
Socket client = server.accept(); // 阻塞
new Thread(() -> handleRequest(client)).start();
}
2. 多路复用模型(NIO)
- 适用场景:高并发(C10K级别)、长连接、低延迟要求的场景。
- 特点:单线程管理多个通道,通过Selector实现事件驱动。
- 关键选择:
- Reactor模式:Netty/MinA等框架默认实现
- Proactor模式:Windows IOCP
- 代码片段:
Selector selector = Selector.open();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
3. 异步I/O模型(AIO)
- 适用场景:文件I/O密集或需要真正异步处理的场景。
- 注意:Linux底层仍用epoll模拟,Windows原生支持更好。
- 典型API:
AsynchronousServerSocketChannel.open()
.bind(new InetSocketAddress(8080))
.accept(null, completionHandler);
4. 混合模型选择建议
模型 | 连接数 | 吞吐量 | 延迟 | 编程复杂度 |
---|---|---|---|---|
BIO | 低 | 中 | 高 | ★☆☆☆☆ |
NIO | 高 | 高 | 低 | ★★★☆☆ |
AIO | 高 | 中高 | 最低 | ★★★★☆ |
5. 生产环境建议
- Web服务:优先选择Netty(Reactor模式)
- 文件传输:考虑AIO(尤其Windows平台)
- 遗留系统:线程池+BIO(改造成本低)
- 关键参数:
- NIO的Selector超时时间建议设置1-100ms
- Worker线程数 = CPU核心数 * (1 + 等待时间/计算时间)
常见问题排查方法
1. 连接超时或拒绝连接
- 可能原因:
- 服务器未启动或监听端口错误
- 防火墙/网络配置阻止连接
- Selector未正确注册通道
- 排查步骤:
// 检查服务器是否监听正确端口 ss -tulnp | grep <端口号> // 测试网络连通性 telnet <IP> <端口>
2. Selector空轮询(CPU 100%)
- 现象:select()立即返回但无就绪事件
- 解决方案:
// 1. 记录空轮询次数 int emptyCount = 0; while (true) { int readyChannels = selector.select(500); if (readyChannels == 0) { emptyCount++; if (emptyCount > MAX_EMPTY_POLL) { // 重建Selector selector = Selector.open(); channels.forEach(ch -> ch.register(selector, ops)); } } }
3. 事件丢失或重复处理
- 注意事项:
- 每次select()后必须调用selectedKeys().iterator()并remove()
- 处理完事件后需重新设置感兴趣的事件
Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> iter = keys.iterator(); while (iter.hasNext()) { SelectionKey key = iter.next(); iter.remove(); // 必须移除 if (key.isReadable()) { // 处理读事件 key.interestOps(SelectionKey.OP_WRITE); // 更新关注事件 } }
4. 内存泄漏
- 常见场景:
- ByteBuffer未释放(特别是DirectBuffer)
- Channel未关闭
- 检测工具:
jmap -histo:live <pid> | grep ByteBuffer
5. 性能瓶颈分析
- 关键指标:
// 监控Selector处理延迟 long start = System.nanoTime(); selector.select(); long cost = System.nanoTime() - start;
6. 线程阻塞问题
- 典型表现:
- select()阻塞导致无法处理新请求
- 建议方案:
- 使用单独的Selector线程
- 设置合理的select超时时间