基本概念
通过前文的学习,我们用java.net包下的API实现了同步阻塞IO模型,我们用java.nio包下的API实现了同步非阻塞IO和IO多路复用模型。
Java NIO(New I/O 或者是 none-block IO)是 Java 1.4 引入的非阻塞 I/O 模型,旨在解决传统阻塞 I/O(BIO)在高并发场景下的性能瓶颈。这些API的底层在JVM层面包装的是对一些系统函数的调用,供给开发者使用的API。
包含了三大组件:通道 (Channel) 缓冲区 (Buffer) 选择器(Selector)
Channel
Channel是数据传输的双向通道,与流(InputStream/OutputStream)不同,Channel同时支持读写操作。

ServerSocketChannel和SocketChannel是网络读写的Channel:
ServerSocketChannel底层封装了一个服务端 的Socket,SocketChannel则封装了一个客户端Socket。
FileChannel则代表了文件读写。
ServerSocketChannel:服务器连接监听
核心功能:
- 绑定本地端口,监听客户端连接请求
- 通过accept()方法来接受客户端连接,返回SocketChannel用于后续通信
- 可以配置非阻塞模式,与Selector配合实现多路复用
用于服务端接收客户端连接使用,相关API如下:
java
初始化
工厂模式创建ServerSocketChannel,增强扩展性
ServerSocketChannel serverChannel = ServerSocketChannel.open();
绑定本地端口
serverChannel.bind(new InetSocketAddress(8080));
配置非阻塞模式,默认是阻塞模式
serverChannel.configureBlocking(false);
=================================================================
接收连接
阻塞模式:若无连接请求,线程会一直阻塞直到有连接到达
SocketChannel clientChannel = serverChannel.accept();
非阻塞模式:立即返回,没有连接请求返回null
SocketChannel clientChannel = serverChannel.accpet();
if(clientChannel != null){
// 通常也将客户端设置为非阻塞模式
clientChannel.configureBlocking(false);
}
=================================================================
关闭通道 释放资源
serverChannel.close();
SocketChannel:客户端与服务端数据通信
核心功能:
- 客户端通过connect()连接服务端
- 服务端通过ServerSocketChannel.accept()返回的SocketChannel与客户端通信
- 支持双向数据读写
- 支持非阻塞模式,可以与Selector配合实现异步IO
主要是用于双向通信,相关API如下
java
创建与连接
SocketChannel clientChannel = SocketChannel.open();
clientChannel.configureBlocking(false);
boolean connected = clientChannel.connect(new InetSocketAddress("localhost",8080));
// 非阻塞模式下,连接可能未立即完成,需检查finishConnect()
if (!connected) {
while (!clientChannel.finishConnect()) {
// 等待连接完成(可在此处理其他任务)
}
}
===========================================================================
数据读写
写入数据(返回写入的字节数)
ByteBuffer buffer = ByteBuffer.wrap("Hello, Server!".getBytes());
int bytesWritten = clientChannel.write(buffer);
读取数据(返回读取的字节数,可能为0)
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int bytesRead = clientChannel.read(readBuffer);
if (bytesRead > 0) {
readBuffer.flip();
byte[] data = new byte[bytesRead];
readBuffer.get(data);
System.out.println("Received: " + new String(data));
}
===========================================================================
关闭通道 释放资源
clientChannel.close()
组件 | 核心功能 | 关键API |
---|---|---|
ServerSocketChannel | 服务端监听端口;接受客户端连接 | open() , bind() , accept() , configureBlocking(false) |
SocketChannel | 客户端连接服务端;双向数据通信 | open() , connect() , read() , write() , finishConnect() , configureBlocking(false) |
Buffer
数据缓冲区,也是一个数据容器,在JAVA NIO中数据都是用Buffer存储的。Channel读取接收到的数据先写到Buffer中,应用程序再从Buffer中读取,Channel写数据也是先写到Buffer中。
为什么要使用缓冲区?
为了通过 Buffer 的批量操作,将 多次小数据量系统调用 合并为 单次大数据量系统调用,减少内核态与用户态切换次数,显著提高网络通信的性能。

ByteBuffer是里面最常用的一个类型,因为字节类型最通用传输最高效。
ByteBuffer
创建ByteBuffer

allocate(int capacity)方法
创建的是一个HeapByteBuffer实例,数据是存储在堆区按容量开辟空间的字节数组中。
- GC 管理:无需手动释放内存,但 GC 可能带来停顿(STW)。
- 访问速度:Java 对象直接访问,速度快(但受 JVM 堆限制)。
- 适用场景:大多数常规 I/O 操作,尤其是数据量较小或需要频繁 GC 的场景。
allocateDirect(int capacity)方法
创建的是一个DirectByteBuffer实例,数据是存储在操作系统直接内存中按容量开辟空间的字节数组中。
- 手动管理 :虽然 DirectByteBuffer 对象本身由 GC 管理,但堆外内存需通过
Cleaner
机制在 GC 时释放(或显式调用sun.misc.Unsafe.freeMemory()
)。 - 访问速度 :
- 写入:比堆缓冲区慢(需通过 JNI 调用本地方法写入堆外内存)。
- 读取:比堆缓冲区快(尤其是涉及 I/O 操作时,可避免数据拷贝)。
- 适用场景:高频、大容量的 I/O 操作(如网络传输、文件读写),尤其是需要零拷贝的场景。
堆缓冲区(HeapByteBuffer) | 直接缓冲区(DirectByteBuffer) | |
---|---|---|
内存分配 | JVM 堆内存(GC 管理) | 操作系统本地内存(需手动释放) |
分配/释放速度 | 快(纯 Java 操作) | 慢(需调用本地方法 malloc /free ) |
I/O 性能 | 低(额外拷贝) | 高(零拷贝优化) |
访问速度 | 快(Java 对象直接访问) | 慢(需通过 JNI 访问堆外内存) |
内存限制 | 受 JVM 堆大小限制(-Xmx) | 受操作系统可用内存限制(可分配 GB 级) |
适用场景 | 小数据量、频繁 GC 的场景 | 大数据量、高频 I/O、零拷贝场景 |
操作ByteBuffer
Buffer类中定义了四个索引,这些索引记录了底层数据元素的状态。

capacity:缓冲区可以容纳的数据元素的最大数量
limit:停止读写的索引
position:当前要读或写的索引,读写操作是从position开始,到limit停止
mark:用于记录position的位置,默认值为-1

flip():由写模式切换为读模式 通过limit = position
compact():由读模式切换为写模式
remaining():计算可读/写大小
操作 | 触发方法 | 索引调整 | 模式变化方向 |
---|---|---|---|
写模式→读模式 | flip() |
limit = position; position=0 |
写 → 读(单向) |
读模式→写模式 | clear() |
position=0; limit=capacity |
读 → 写(完全重置) |
读模式→写模式 | compact() |
移动未读数据;调整position/limit | 读 → 写(保留未读数据) |
- 向 ByteBuffer 中写数据:核心方法是put
- put(byte[] src)
- put(byte[] src, int offset, int length)
- put(ByteBuffer src)
- 从 ByteBuffer中读数据:核心方法是 get
- get(byte[] dst)
- get(byte[] dst, int offset, int length)
Selector选择器
允许单个线程高效管理多个通道(Channel),实现I/O多路复用,从而显著提升网络应用的性能和可扩展性。
selector底层封装的是IO多路复用模型 epoll ,主要功能是将Channel注册到Selector上并指定要监听的事件,selector会不断轮询注册在其上的Channel,检测其事件是否就绪;Selector每次返回已就绪Channel对应的SelectionKey,通过SelectionKey可获取事件的详细信息,然后进行后续的操作。(单线程通过事件驱动监控多个通道,仅当通道发生指定事件(如可读,可写,新连接)时才处理,避免空轮询和线程切换)
Selector支持四种事件类型,通过SelectionKey的常量定义:
- OP_ACCEPT:服务端接收新连接
- OP_CONNECT:客户端完成连接
- OP_READ:通道数据可读
- OP_WRITE:通道可写(谨慎使用,避免频繁触发)
使用Selector线程循环需要以下步骤:
- 调用 select()阻塞当前线程 等待事件发生
- 遍历就绪事件(selectedKeys),分发处理
- 避免线程阻塞在 I/O 操作上面,充分利用空闲事件处理其他通道
相关API 如下:
Selector类:
java
创建Selector实例,底层调用系统默认的SelectorProvider(如Linux的EPollSelectorProvider)。
Selector.open()
阻塞等待事件,超时返回。返回值为就绪通道数量。
select(long timeout)
返回就绪事件的SelectionKey集合,需手动遍历处理。
selectedKeys()
唤醒阻塞的select(),用于优雅退出事件循环。
wakeup()
SelectionKey类:
java
设置通道感兴趣的事件(如OP_READ | OP_WRITE)。
interestOps(int ops)
获取当前就绪的事件(如OP_READ)。
readyOps()
绑定用户对象(如Socket实例),便于事件处理时获取上下文。
attach(Object obj)
检查键是否有效(如通道关闭后键自动失效)。
isValid()
NIO完整案例
根据上述学的三大组件写一个完整的Java NIO 案例
java
/**
* @author XJunFy
*/
public class NioServer {
public static void main(String[] args) {
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);
Selector selector = Selector.open();
SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while(true){
int select = selector.select();
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()){
SelectionKey key = keyIterator.next();
keyIterator.remove();
processingKey(key,selector);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void processingKey(SelectionKey key, Selector selector) throws IOException {
if (key.isValid()){
if (key.isAcceptable()){
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector,SelectionKey.OP_READ);
return;
}
if (key.isReadable()){
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int read = socketChannel.read(readBuffer);
if (read > 0){
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String msg = new String(bytes, Charset.defaultCharset());
System.out.println("服务端收到来自客户端的数据:" + msg);
ByteBuffer sendBuffer = ByteBuffer.allocate(256);
sendBuffer.put("hello nio client,i am nio server\n".getBytes(StandardCharsets.UTF_8));
sendBuffer.flip();
socketChannel.write(sendBuffer);
}
return;
}
}
}
}