在 Java IO 编程领域,传统的 BIO(Blocking IO)模型因 "一连接一线程" 的特性,在高并发场景下存在严重的性能瓶颈。而 Java NIO(New Input/Output,JDK 1.4 引入)通过非阻塞 IO 、多路复用等核心机制,彻底解决了 BIO 的性能问题,成为 Netty、Tomcat 等高性能框架的底层技术基石。本文将从 NIO 的核心组件入手,深入解析其工作原理、非阻塞模型优势,并结合实战场景讲解 NIO 的应用技巧,帮助开发者掌握高并发 IO 编程的核心能力。
一、为什么需要 Java NIO?------BIO 的痛点与 NIO 的优势
在了解 NIO 之前,首先需要明确传统 BIO 模型的局限性,这是理解 NIO 设计初衷的关键。
1.1 传统 BIO 模型的痛点
BIO(Blocking IO)即阻塞式 IO,其核心特点是 "一连接一线程":每当有一个客户端连接请求,服务器就需要创建一个新线程来处理该连接的 IO 操作(如读取数据、发送响应)。这种模型在低并发场景下简单易用,但在高并发场景下存在三大致命问题:
(1)线程资源耗尽
每个线程都需要占用一定的内存(默认栈大小 1MB)和 CPU 资源,若同时存在上万甚至十万级别的客户端连接,服务器会因创建过多线程导致内存溢出(OOM)或 CPU 上下文切换频繁,系统性能急剧下降。
(2)IO 阻塞导致线程闲置
在 BIO 模型中,线程在执行 IO 操作(如read()、write())时会处于阻塞状态,例如:
- 调用inputStream.read()读取数据时,若客户端未发送数据,线程会一直阻塞等待,直到有数据到达;
- 调用socket.accept()监听连接时,若没有新连接请求,线程也会阻塞。
大量阻塞的线程处于闲置状态,严重浪费系统资源。
(3)扩展性差
BIO 模型无法应对高并发场景下的连接增长,当连接数超过线程池最大容量时,新连接会被拒绝,无法满足互联网应用(如电商秒杀、直播互动)的高并发需求。
1.2 NIO 的核心优势
NIO(New Input/Output)通过三大核心特性,完美解决了 BIO 的痛点:
(1)非阻塞 IO(Non-blocking IO)
NIO 的 IO 操作(读取、写入、连接)均为非阻塞模式:
- 读取数据时,若没有数据到达,read()方法会立即返回-1,线程无需阻塞等待;
- 写入数据时,若缓冲区已满,write()方法会立即返回写入的字节数,线程可继续处理其他任务;
- 监听连接时,accept()方法仅在有新连接时才返回,否则不阻塞线程。
非阻塞特性让线程可以同时处理多个 IO 任务,避免了线程闲置。
(2)IO 多路复用(IO Multiplexing)
NIO 通过Selector(选择器)实现 IO 多路复用:一个线程可以通过Selector同时监控多个Channel(通道)的 IO 事件(如 "数据可读""新连接到达"),当某个Channel触发事件时,线程再去处理对应的 IO 操作。
这种 "一线程多通道" 的模型,彻底解决了 BIO "一连接一线程" 的资源浪费问题,即使面对十万级连接,也只需少量线程即可处理。
(3)面向缓冲区(Buffer-oriented)
NIO 的所有 IO 操作都基于Buffer(缓冲区)实现:数据读取时,先从Channel读取到Buffer;数据写入时,先从Buffer写入到Channel。缓冲区的设计不仅减少了数据拷贝次数(相比 BIO 的流模型),还支持随机访问、部分数据操作,提升了 IO 效率。
二、Java NIO 核心组件解析
Java NIO 的核心由三大组件构成:Buffer(缓冲区)、Channel(通道)、Selector(选择器),三者协同工作实现非阻塞 IO 与多路复用。
2.1 Buffer:NIO 的 "数据容器"
Buffer是 NIO 中用于存储数据的容器,本质是一个可读写的字节数组。所有 IO 操作都必须通过Buffer进行 ------ 读取数据时,数据从Channel流入Buffer;写入数据时,数据从Buffer流入Channel。
2.1.1 Buffer 的核心属性
Buffer类(抽象类)包含四个核心属性,用于控制缓冲区的读写状态:
- capacity:缓冲区的容量(初始化时确定,不可修改),表示缓冲区可存储的最大数据量;
- position:当前读写位置,初始化时为0,读取 / 写入数据后自动移动;
- limit:读写的边界,读取模式下limit等于缓冲区中实际数据的长度,写入模式下limit等于capacity;
- mark:标记位置,通过mark()方法标记当前position,通过reset()方法恢复到mark位置(用于重复读取数据)。
四个属性的关系为:0 ≤ mark ≤ position ≤ limit ≤ capacity。
2.1.2 Buffer 的核心方法
以最常用的ByteBuffer(字节缓冲区)为例,核心方法如下:
|------------------------|------------------------------------------------------------------|
| 方法 | 功能描述 |
| allocate(int capacity) | 静态方法,创建一个容量为capacity的直接缓冲区(内存分配在堆外,减少拷贝) |
| wrap(byte[] array) | 静态方法,将字节数组包装为缓冲区(缓冲区与数组共享内存) |
| put(byte b) | 向缓冲区写入一个字节,position加 1 |
| put(byte[] src) | 向缓冲区写入字节数组,position增加数组长度 |
| get() | 从缓冲区读取一个字节,position加 1 |
| get(byte[] dst) | 从缓冲区读取数据到字节数组,position增加读取的字节数 |
| flip() | 切换为读取模式:limit = position,position = 0,mark = -1 |
| clear() | 切换为写入模式:position = 0,limit = capacity,mark = -1(数据未清空,仅重置指针) |
| rewind() | 重置读取位置:position = 0,mark = -1(用于重复读取数据) |
| remaining() | 返回剩余可读写的字节数:limit - position |
2.1.3 Buffer 的使用流程(读写数据)
使用Buffer的核心流程分为 "写入数据→切换读取模式→读取数据→切换写入模式" 四步,示例代码如下:
import java.nio.ByteBuffer;
public class BufferExample {
public static void main(String[] args) {
// 1. 创建缓冲区(容量为1024字节)
ByteBuffer buffer = ByteBuffer.allocate(1024);
System.out.println("初始化状态:" + buffer);
// 输出:java.nio.HeapByteBuffer[pos=0 lim=1024 cap=1024]
// 2. 写入数据(切换为写入模式,clear()可省略,初始化默认是写入模式)
String data = "Hello Java NIO";
buffer.put(data.getBytes());
System.out.println("写入数据后:" + buffer);
// 输出:java.nio.HeapByteBuffer[pos=14 lim=1024 cap=1024](14为字符串字节数)
// 3. 切换为读取模式(关键步骤,否则无法正确读取数据)
buffer.flip();
System.out.println("切换读取模式后:" + buffer);
// 输出:java.nio.HeapByteBuffer[pos=0 lim=14 cap=1024]
// 4. 读取数据
byte[] readData = new byte[buffer.remaining()]; // 剩余可读取字节数
buffer.get(readData);
System.out.println("读取的数据:" + new String(readData)); // 输出:Hello Java NIO
System.out.println("读取数据后:" + buffer);
// 输出:java.nio.HeapByteBuffer[pos=14 lim=14 cap=1024]
// 5. 切换为写入模式(准备再次写入数据)
buffer.clear();
System.out.println("切换写入模式后:" + buffer);
// 输出:java.nio.HeapByteBuffer[pos=0 lim=1024 cap=1024]
}
}
2.1.4 直接缓冲区与非直接缓冲区
Buffer分为两种类型:
- 直接缓冲区(Direct Buffer):通过allocateDirect(int capacity)创建,内存分配在 JVM 堆外(操作系统内核空间),IO 操作时无需将数据从堆内拷贝到堆外,性能更高,适用于频繁 IO 的场景;
- 非直接缓冲区(Non-direct Buffer):通过allocate(int capacity)创建,内存分配在 JVM 堆内,IO 操作时需要进行一次堆内到堆外的数据拷贝,性能较低,适用于少量数据操作。
注意:直接缓冲区的创建和销毁成本较高,建议复用(如通过对象池管理),避免频繁创建。
2.2 Channel:NIO 的 "数据通道"
Channel(通道)是 NIO 中用于连接数据源与目标的 "管道",所有 IO 操作都必须通过Channel进行。与 BIO 的流(InputStream/OutputStream)相比,Channel具有两大特点:
- 双向性:Channel既可读又可写(流是单向的,如InputStream只能读,OutputStream只能写);
- 非阻塞性:Channel支持非阻塞模式,可配合Selector实现多路复用。
2.2.1 常见的 Channel 实现类
Java NIO 提供了多种Channel实现,适用于不同的 IO 场景:
|---------------------|--------------------------------------------------|
| Channel 类型 | 功能描述 |
| SocketChannel | 客户端 TCP 通道,用于与服务器建立 TCP 连接并进行数据读写 |
| ServerSocketChannel | 服务器 TCP 通道,用于监听客户端 TCP 连接请求,可创建SocketChannel处理连接 |
| DatagramChannel | UDP 通道,用于发送和接收 UDP 数据包(无连接) |
| FileChannel | 文件通道,用于读取和写入本地文件(仅支持阻塞模式,不支持非阻塞) |
2.2.2 Channel 的核心方法(以 SocketChannel 为例)
以客户端SocketChannel为例,核心方法如下:
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class SocketChannelExample {
public static void main(String[] args) throws Exception {
// 1. 打开SocketChannel(非阻塞模式)
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false); // 设置为非阻塞模式
// 2. 连接服务器(非阻塞连接,立即返回,需通过finishConnect()检查是否完成)
socketChannel.connect(new InetSocketAddress("localhost", 8080));
while (!socketChannel.finishConnect()) {
// 连接未完成时,可处理其他任务(非阻塞特性)
System.out.println("等待连接完成,处理其他任务...");
}
// 3. 写入数据(通过Buffer)
String sendData = "Hello Server";
ByteBuffer writeBuffer = ByteBuffer.wrap(sendData.getBytes());
while (writeBuffer.hasRemaining()) {
socketChannel.write(writeBuffer); // 非阻塞写入,返回实际写入的字节数
}
// 4. 读取数据(通过Buffer)
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = socketChannel.read(readBuffer); // 非阻塞读取,无数据时返回-1
if (readBytes > 0) {
readBuffer.flip(); // 切换为读取模式
byte[] receiveData = new byte[readBuffer.remaining()];
readBuffer.get(receiveData);
System.out.println("收到服务器响应:" + new String(receiveData));
}
// 5. 关闭通道
socketChannel.close();
}
}
2.3 Selector:NIO 的 "事件调度器"
Selector(选择器)是 NIO 实现 IO 多路复用的核心组件,其核心作用是:一个线程通过 Selector 同时监控多个 Channel 的 IO 事件,当事件触发时,线程再处理对应的 Channel。
2.3.1 Selector 的核心概念
- 事件(SelectionKey):Selector监控的 IO 事件类型,共四种:
-
- SelectionKey.OP_READ:通道可读事件(有数据可读取);
-
- SelectionKey.OP_WRITE:通道可写事件(缓冲区可写入数据);
-
- SelectionKey.OP_ACCEPT:连接接收事件(ServerSocketChannel有新连接);
-
- SelectionKey.OP_CONNECT:连接完成事件(SocketChannel连接服务器完成)。
- 注册(register):Channel需要通过register(Selector selector, int ops)方法注册到Selector,并指定要监控的事件类型;
- 选择(select):Selector通过select()方法阻塞等待事件触发,返回触发事件的Channel数量;
- ** SelectionKey **:Channel注册到Selector后返回的 "事件密钥",包含Channel、Selector、监控的事件类型等信息,可通过SelectionKey获取对应的Channel。
2.3.2 Selector 的使用流程(服务器端示例)
Selector的核心使用流程分为 "创建 Selector→注册 Channel→循环监听事件→处理事件" 四步,以下是服务器端ServerSocketChannel配合Selector处理多客户端连接的示例:
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 NioServer {
public static void main(String[] args) throws IOException {
// 1. 创建ServerSocketChannel(非阻塞模式)
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false); // 非阻塞模式
serverSocketChannel.bind(new InetSocketAddress(8080)); // 绑定端口
// 2. 创建Selector
Selector selector = Selector.open();
// 3. 将ServerSocketChannel注册到Selector,监控"连接接收事件"(OP_ACCEPT)
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO服务器启动,监听端口8080...");
// 4. 循环监听事件(核心逻辑)
while (true) {
// 4.1 阻塞等待事件触发(返回触发事件的Channel数量,0表示超时)
int selectCount = selector.select(); // 无事件时阻塞,有事件时返回
if (selectCount == 0) {
continue;
}
// 4.2 获取所有触发事件的SelectionKey
Set selectionKeys = selector.selectedKeys();
IteratorKey> iterator = selectionKeys.iterator();
// 4.3 遍历处理每个事件
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 4.3.1 处理"连接接收事件"(ServerSocketChannel)
if (key.isAcceptable()) {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
// 接受新连接(非阻塞,仅在有新连接时返回)
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false); // 客户端Channel设为非阻塞
// 将客户端Channel注册到Selector,监控"可读事件"(OP_READ)
clientChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
System.out.println("新客户端连接:" + clientChannel.getRemoteAddress());
}
// 4.3.2 处理"可读事件"(SocketChannel)
else if (key.isReadable()) {
SocketChannel clientChannel = (SocketChannel) key.channel();
// 获取Channel关联的Buffer(注册时通过attach()附加)
ByteBuffer buffer = (ByteBuffer) key.attachment();
// 读取数据(非阻塞,无数据时返回-1)
int readBytes = clientChannel.read(buffer);
if (readBytes > 0) {
buffer.flip(); // 切换为读取模式
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String clientData = new String(data);
System.out.println("收到客户端" + clientChannel.getRemoteAddress() + "数据:" + clientData);
// 向客户端发送响应(非阻塞写入)
String response = "服务器已收到:" + clientData;
ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());
clientChannel.write(responseBuffer);
// 重置Buffer,准备下次读取
buffer.clear();
}
// 客户端关闭连接(readBytes == -1)
else if (readBytes 0) {
System.out.println("客户端" + clientChannel.getRemoteAddress() + "断开连接");
key.cancel(); // 取消SelectionKey
clientChannel.close(); // 关闭Channel
}
}
// 4.3.3 移除已处理的SelectionKey(避免重复处理)
iterator.remove();
}
}
}
}
2.3.3 Selector 的关键注意事项
- 线程安全性:Selector是线程安全的,但Channel的注册(register)和事件处理需避免并发操作,建议由同一线程处理;
- SelectionKey 的移除:处理完事件后,必须通过iterator.remove()移除SelectionKey,否则下次select()会重复处理该事件;
- 非阻塞模式要求:只有设置为非阻塞模式的Channel(SocketChannel、ServerSocketChannel、DatagramChannel)才能注册到Selector,FileChannel不支持非阻塞模式,无法注册;
- select () 的阻塞与唤醒:selector.select()会阻塞线程,可通过selector.wakeup()唤醒线程(如关闭服务器时),避免线程一直阻塞。
三、Java NIO 的非阻塞 IO 模型深度解析
NIO 的高性能核心源于其 "非阻塞 IO + 多路复用" 模型,理解该模型的工作流程,是掌握 NIO 的关键。
3.1 NIO 非阻塞 IO 模型的工作流程
以服务器端处理多客户端连接为例,NIO 模型的工作流程可分为以下四步:
步骤 1:初始化组件
- 创建ServerSocketChannel,设为非阻塞模式,绑定端口;
- 创建Selector,将ServerSocketChannel注册到Selector,监控OP_ACCEPT事件;
- 启动一个线程,循环执行selector.select()等待事件。
步骤 2:监听连接事件
- 客户端发起连接请求时,ServerSocketChannel的OP_ACCEPT事件触发;
- selector.select()返回 1,线程从阻塞中唤醒,获取SelectionKey;
- 处理OP_ACCEPT事件:调用serverSocketChannel.accept()获取SocketChannel(客户端通道),设为非阻塞模式,注册到Selector并监控OP_READ事件。
步骤 3:监听可读事件
- 客户端发送数据时,SocketChannel的OP_READ事件触发;
- selector.select()返回 1,线程唤醒,获取对应的SelectionKey;
- 处理OP_READ事件:从SocketChannel读取数据到Buffer,处理数据后向客户端发送响应,重置Buffer准备下次读取。
步骤 4:客户端断开连接
- 客户端关闭连接时,SocketChannel的read()方法返回-1;
- 处理断开逻辑:取消SelectionKey,关闭SocketChannel,释放资源。
3.2 NIO 与 BIO 的性能对比
通过一个简单的性能测试(模拟 1000 个客户端连接,每个客户端发送 10 次数据),对比 NIO 与 BIO 的资源占用和响应时间:
|-------------|-----------------------|---------------------|
| 指标 | BIO 模型(线程池实现) | NIO 模型(Selector 实现) |
| 线程数量 | 1000+(一连接一线程) | 1(一线程多通道) |
| 内存占用(JVM 堆) | 约 1GB(1000 线程 ×1MB 栈) | 约 50MB(仅 1 线程) |
| 平均响应时间 | 500ms+ | 50ms+ |
| 最大并发支持 | 约 1 万连接(线程池上限) | 约 10 万连接(无线程限制) |
从测试结果可见,NIO 在高并发场景下的资源占用和响应时间远优于 BIO,这也是 NIO 成为高性能框架底层技术的核心原因。
四、Java NIO 实战:实现一个简单的高并发服务器
基于前面的理论知识,我们通过 NIO 实现一个支持高并发的 TCP 服务器,具备以下功能:
- 支持同时处理上万客户端连接;
- 非阻塞接收客户端连接和数据;
- 接收客户端数据后,返回 "服务器已收到" 的响应;
- 客户端断开连接时自动释放资源。
4.1 服务器端完整代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
public class HighConcurrencyNioServer {
// 服务器端口
private static final int PORT = 8080;
// 缓冲区大小
private static final int BUFFER_SIZE = 1024;
// 客户端连接计数
private static final AtomicInteger clientCount = new AtomicInteger(0);
public static void main(String[] args) throws IOException {
// 1. 初始化ServerSocketChannel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false); // 非阻塞模式
serverChannel.bind(new InetSocketAddress(PORT)); // 绑定端口
// 2. 初始化Selector
Selector selector = Selector.open();
// 3. 注册ServerSocketChannel到Selector,监控OP_ACCEPT事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("高并发NIO服务器启动成功,监听端口:" + PORT);
System.out.println("----------------------------------------");
// 4. 事件循环(核心处理逻辑)
while (true) {
// 阻塞等待事件(超时时间100ms,避免无限阻塞)
int selectCount = selector.select(100);
if (selectCount == 0) {
continue;
}
// 获取触发事件的SelectionKey集合
SetKeys = selector.selectedKeys();
Iterator> iterator = selectionKeys.iterator();
// 遍历处理每个事件
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 处理连接接收事件(OP_ACCEPT)
if (key.isAcceptable()) {
handleAcceptEvent(key, selector);
}
// 处理数据可读事件(OP_READ)
else if (key.isReadable()) {
handleReadEvent(key);
}
// 移除已处理的SelectionKey,避免重复处理
iterator.remove();
}
}
}
/**
* 处理连接接收事件(ServerSocketChannel)
*/
private static void handleAcceptEvent(SelectionKey key, Selector selector) throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
// 接受新客户端连接(非阻塞,仅在有新连接时返回)
SocketChannel clientChannel = serverChannel.accept();
if (clientChannel == null) {
return;
}
// 配置客户端Channel为非阻塞模式
clientChannel.configureBlocking(false);
// 为客户端Channel创建Buffer,并附加到SelectionKey(用于后续读取)
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
clientChannel.register(selector, SelectionKey.OP_READ, buffer);
// 客户端连接计数自增
int count = clientCount.incrementAndGet();
String clientAddr = clientChannel.getRemoteAddress().toString();
System.out.println("新客户端连接:" + clientAddr + ",当前连接数:" + count);
}
/**
* 处理数据可读事件(SocketChannel)
*/
private static void handleReadEvent(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
String clientAddr = clientChannel.getRemoteAddress().toString();
try {
// 读取客户端数据(非阻塞,无数据时返回-1)
int readBytes = clientChannel.read(buffer);
// 情况1:客户端发送数据(readBytes > 0)
if (readBytes > 0) {
buffer.flip(); // 切换为读取模式
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String clientData = new String(data).trim();
System.out.println("收到客户端" + clientAddr + "数据:" + clientData);
// 向客户端发送响应
String response = "服务器已收到(" + System.currentTimeMillis() + "):" + clientData + "\n";
ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());
clientChannel.write(responseBuffer);
// 重置Buffer,准备下次读取
buffer.clear();
}
// 情况2:客户端断开连接(readBytes else if (readBytes closeClientChannel(key, clientChannel);
}
} catch (IOException e) {
// 客户端异常断开(如网络中断)
System.err.println("客户端" + clientAddr + "异常断开:" + e.getMessage());
closeClientChannel(key, clientChannel);
}
}
/**
* 关闭客户端Channel,释放资源
*/
private static void closeClientChannel(SelectionKey key, SocketChannel clientChannel) throws IOException {
// 取消SelectionKey,从Selector中移除
key.cancel();
// 关闭客户端Channel
clientChannel.close();
// 客户端连接计数自减
int count = clientCount.decrementAndGet();
String clientAddr = clientChannel.getRemoteAddress().toString();
System.out.println("客户端" + clientAddr + "断开连接,当前连接数:" + count);
}
}
4.2 客户端测试代码(模拟多客户端)
为验证服务器的高并发支持,我们编写一个客户端工具类,模拟 1000 个客户端同时连接并发送数据:
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class NioClientSimulator {
// 服务器地址和端口
private static final String SERVER_HOST = "localhost";
private static final int SERVER_PORT = 8080;
// 模拟客户端数量
private static final int CLIENT_NUM = 1000;
// 每个客户端发送数据次数
private static final int SEND_TIMES = 5;
public static void main(String[] args) throws InterruptedException {
// 线程池(用于模拟多客户端)
ExecutorService executorService = Executors.newFixedThreadPool(CLIENT_NUM);
// 倒计时锁(等待所有客户端完成)
CountDownLatch countDownLatch = new CountDownLatch(CLIENT_NUM);
System.out.println("开始模拟" + CLIENT_NUM + "个客户端连接服务器...");
// 模拟每个客户端
for (int i = 0; i i++) {
int clientId = i;
executorService.submit(() -> {
try {
// 打开SocketChannel,连接服务器
SocketChannel clientChannel = SocketChannel.open();
clientChannel.connect(new InetSocketAddress(SERVER_HOST, SERVER_PORT));
System.out.println("客户端" + clientId + "连接服务器成功");
// 每个客户端发送SEND_TIMES次数据
for (int j = 0; j j++) {
// 发送数据
String sendData = "客户端" + clientId + "的第" + (j + 1) + "条数据";
ByteBuffer buffer = ByteBuffer.wrap(sendData.getBytes());
clientChannel.write(buffer);
// 读取服务器响应
ByteBuffer responseBuffer = ByteBuffer.allocate(1024);
int readBytes = clientChannel.read(responseBuffer);
if (readBytes > 0) {
responseBuffer.flip();
byte[] responseData = new byte[responseBuffer.remaining()];
responseBuffer.get(responseData);
System.out.println("客户端" + clientId + "收到响应:" + new String(responseData).trim());
}
// 间隔100ms发送下一条数据
Thread.sleep(100);
}
// 关闭客户端Channel
clientChannel.close();
System.out.println("客户端" + clientId + "断开连接");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 倒计时锁减1
countDownLatch.countDown();
}
});
}
// 等待所有客户端完成
countDownLatch.await();
System.out.println("所有客户端模拟完成");
executorService.shutdown();
}
}
4.3 实战效果验证
- 启动HighConcurrencyNioServer服务器;
- 启动NioClientSimulator客户端,模拟 1000 个客户端连接;
- 观察服务器日志,可看到:
-
- 服务器仅用 1 个线程处理所有客户端连接;
-
- 客户端连接数稳定达到 1000,无连接拒绝;
-
- 数据读取和响应正常,无阻塞或超时。
这验证了 NIO 在高并发场景下的稳定性和高性能。
五、Java NIO 的常见问题与解决方案
在使用 NIO 开发时,开发者常会遇到一些问题,如事件重复处理、缓冲区溢出、连接泄漏等,本节总结常见问题及解决方案。
5.1 SelectionKey 重复处理
问题现象:Selector的selectedKeys集合中,已处理的SelectionKey未被移除,导致下次select()时重复处理该事件。
解决方案:
- 处理完事件后,必须通过iterator.remove()从selectedKeys集合中移除SelectionKey;
- 避免直接遍历selectedKeys集合(如for-each循环),必须使用Iterator遍历并移除。
5.2 缓冲区数据读取不完整
问题现象:Channel.read(buffer)返回的字节数小于实际数据长度,导致数据读取不完整(非阻塞模式下常见)。
解决方案:
- 循环调用read()方法,直到buffer.remaining() == 0(缓冲区满)或read()返回-1(无数据);
- 若数据长度固定,可通过buffer.remaining()判断是否读取完整;
- 若数据长度不固定,可在数据开头添加 "长度字段",先读取长度,再读取对应长度的数据。
示例代码:
// 读取完整数据(假设数据开头4字节为长度字段)
public static byte[] readCompleteData(SocketChannel channel, ByteBuffer buffer) throws IOException {
// 第一步:读取长度字段(4字节)
while (buffer.position()
if (channel.read(buffer) {
throw new IOException("客户端断开连接");
}
}
buffer.flip();
int dataLength = buffer.getInt(); // 获取数据长度
buffer.clear();
// 第二步:读取指定长度的数据
ByteBuffer dataBuffer = ByteBuffer.allocate(dataLength);
while (dataBuffer.remaining() > 0) {
if (channel.read(dataBuffer) {
throw new IOException("客户端断开连接");
}
}
dataBuffer.flip();
byte[] data = new byte[dataBuffer.remaining()];
dataBuffer.get(data);
return data;
}
5.3 连接泄漏(资源未释放)
问题现象:客户端断开连接时,未及时关闭SocketChannel和取消SelectionKey,导致资源泄漏,长期运行会耗尽文件描述符。
解决方案:
- 客户端断开连接(read()返回-1)或异常时,必须调用key.cancel()和channel.close();
- 服务器关闭时,遍历所有SelectionKey,关闭对应的Channel;
- 使用try-with-resources语法自动关闭Channel和Selector(JDK 7+)。
5.4 Selector 唤醒问题
问题现象:调用selector.select()后,线程一直阻塞,无法通过selector.wakeup()唤醒(如服务器关闭时)。
解决方案:
- 避免在select()阻塞期间调用selector.close(),应先调用wakeup()唤醒线程,再关闭Selector;
- 使用select(long timeout)设置超时时间,避免无限阻塞;
- 确保wakeup()在select()阻塞期间调用,否则wakeup()会无效(下次select()会立即返回)。
服务器关闭示例代码:
// 安全关闭Selector和Channel
public static void shutdownServer(Selector selector, ServerSocketChannel serverChannel) throws IOException {
// 1. 唤醒Selector线程
selector.wakeup();
// 2. 关闭所有客户端Channel
SetKey> allKeys = selector.keys();
for (