# NIO
作者:Ethan.Yang
博客:https://blog.ethanyang.cn (opens new window)
Java NIO是Java 1.4引入的一套全新的IO API,和传统的阻塞IO(BIO)相比,NIO支持非阻塞IO操作,极大提升了高并发场景下的性能和资源利用率
# 一、NIO的设计理念与优势
- 非阻塞IO 线程可以发起IO请求后,不必等待操作完成就继续执行,避免线程被阻塞。
- 多路复用
通过
Selector,一个线程可以同时监听多个通道(Channel),高效管理多个连接。 - 基于缓冲区(Buffer) 数据的读写是围绕Buffer展开的,应用程序直接操作缓冲区,降低数据拷贝成本。
- 面向通道(Channel) 代替传统的流,支持读写数据,支持非阻塞模式。
# 二、Java NIO核心组件详解
# Channel
作用
Channel 是Java NIO中数据传输的核心接口,类似于传统IO中的流,但区别在于它是双向的、可读写的,并且支持阻塞和非阻塞两种模式。它负责连接实体和数据的传输。
设计
Channel是一个接口,位于java.nio.channels包。- 继承自
Closeable接口,支持关闭操作。
源码接口定义(简化版):
public interface Channel extends Closeable {
boolean isOpen();
void close() throws IOException;
}
1
2
3
4
2
3
4
主要实现类
| 类名 | 说明 |
|---|---|
FileChannel | 处理文件数据的通道 |
SocketChannel | TCP客户端Socket通道,支持连接远程服务器 |
ServerSocketChannel | TCP服务器端Socket通道,监听连接请求 |
DatagramChannel | UDP数据报通道 |
关键方法示例(以SocketChannel为例)
// 打开SocketChannel
SocketChannel sc = SocketChannel.open();
// 设置非阻塞模式
sc.configureBlocking(false);
// 连接远程服务器
sc.connect(new InetSocketAddress("localhost", 8080));
// 读写操作通过ByteBuffer进行
ByteBuffer buf = ByteBuffer.allocate(1024);
int bytesRead = sc.read(buf);
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
内部设计细节
- 其底层依赖操作系统的Socket、File等系统资源,通过
FileDescriptor与本地系统交互。 - 通过
SelectableChannel抽象类支持注册到Selector,实现多路复用。
SocketChannel继承链(简化):
java.lang.Object
└─ java.nio.channels.spi.AbstractSelectableChannel
└─ java.nio.channels.SocketChannel
1
2
3
2
3
# Buffer
# 作用:
Buffer是数据的容器,是NIO读写数据的中介。所有NIO数据读写都是基于Buffer进行的。它为数据操作提供统一的API,并负责维护读写位置、容量等状态。
# 设计:
- 抽象类
java.nio.Buffer是所有具体Buffer的父类。 - 常用实现包括
ByteBuffer,CharBuffer,IntBuffer等,分别针对不同数据类型。
核心属性:
| 属性 | 说明 |
|---|---|
| capacity | 容量,Buffer最大存储量 |
| position | 当前读写位置 |
| limit | 读写的边界,不能越界 |
典型Buffer状态转换方法:
flip():切换读写模式,limit=position,position=0clear():清空Buffer,准备写入rewind():回到Buffer起始位置,重新读写
# 代码示例:
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Hello".getBytes());
buffer.flip(); // 准备读
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
System.out.println(new String(bytes)); // 输出Hello
1
2
3
4
5
6
2
3
4
5
6
# 源码核心方法(以ByteBuffer为例):
ByteBuffer是Buffer的子类,重点管理字节数据。
put(byte b)写入一个字节,并将position向前移动get()读取当前位置的字节,并将position向前移动
源码中有大量方法重载来支持不同读写模式和内存管理(堆内存 vs 直接内存)。
# Selector
# 作用:
Selector是Java NIO实现非阻塞IO和多路复用的关键。它允许一个线程监听多个SelectableChannel的事件(如连接就绪、数据可读等),极大提高服务器的并发处理能力。
# 设计:
Selector是一个抽象类,位于java.nio.channels包。
public abstract class Selector implements Closeable {
public static Selector open() throws IOException {...}
public abstract boolean isOpen();
public abstract void close() throws IOException;
public abstract int select() throws IOException;
public abstract int select(long timeout) throws IOException;
public abstract Set<SelectionKey> selectedKeys();
public abstract Set<SelectionKey> keys();
public abstract void wakeup();
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# 工作流程:
- 通过
select()方法阻塞等待事件发生。 - 事件发生后,通过
selectedKeys()获取就绪事件的集合。 - 对每个
SelectionKey处理对应的事件(连接、读写等)。 - 通过
wakeup()可中断阻塞等待。
# 源码设计亮点:
Selector的具体实现依赖于操作系统的IO多路复用机制(Linux使用epoll,Windows使用IOCP或select)。- JDK 11中
SelectorProvider提供不同平台的Selector实现。
# SelectionKey
# 作用:
SelectionKey代表SelectableChannel注册到Selector后与该Selector之间的关联关系,表示该通道当前的状态和感兴趣的事件。
# 设计:
- 作为事件注册和事件状态的载体。
- 包含4类事件操作标志:
| 标志 | 说明 |
|---|---|
OP_ACCEPT | 服务器Socket监听连接事件 |
OP_CONNECT | 客户端Socket连接完成事件 |
OP_READ | 通道数据可读事件 |
OP_WRITE | 通道可写事件 |
- 通过
interestOps()设置关注事件,通过readyOps()获取就绪事件。
源码简化接口:
public abstract class SelectionKey {
public abstract SelectableChannel channel();
public abstract Selector selector();
public abstract int interestOps();
public abstract SelectionKey interestOps(int ops);
public abstract int readyOps();
}
1
2
3
4
5
6
7
2
3
4
5
6
7
# 三、总结
| 组件 | 核心作用 | 设计亮点和源码相关 |
|---|---|---|
| Channel | 数据传输通道,支持非阻塞,基于操作系统底层资源 | 抽象接口,具体实现依赖SelectableChannel和操作系统FD |
| Buffer | 数据缓冲区,维护数据读写状态,提供高效操作接口 | 抽象类Buffer+具体实现如ByteBuffer,支持堆和直接内存 |
| Selector | 事件多路复用器,监听多个通道的IO事件 | 抽象类,具体实现依赖操作系统的多路复用API(epoll、kqueue等) |
| SelectionKey | 通道与选择器的绑定信息,携带关注事件和就绪事件状态 | 包含事件操作标志,管理事件注册与触发 |
# 四、NIO工作流程
- 打开通道
创建通道对象,如
ServerSocketChannel.open()。 - 配置非阻塞模式
通过
channel.configureBlocking(false)将通道设置为非阻塞。 - 绑定端口,监听请求 服务器通道绑定端口,准备接收连接。
- 创建Selector 选择器用于监听通道事件。
- 通道注册到Selector
将通道注册感兴趣的事件(如
OP_ACCEPT、OP_READ等)。 - 轮询Selector,处理事件
调用
selector.select()阻塞等待事件发生,遍历selectedKeys处理事件。
# 五、代码示例
# 服务器端(非阻塞NIO)
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
public class NIOServer {
public static void main(String[] args) throws IOException {
// 1. 打开服务器通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false); // 非阻塞
// 2. 打开选择器
Selector selector = Selector.open();
// 3. 注册服务器通道到选择器,监听连接事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server started on port 8080...");
while (true) {
// 4. 选择器阻塞等待就绪事件
int readyChannels = selector.select();
if (readyChannels == 0) continue;
// 5. 获取就绪事件集合
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove();
if (key.isAcceptable()) {
// 6. 处理连接请求
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = ssc.accept();
clientChannel.configureBlocking(false);
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("Accepted connection from " + clientChannel);
} else if (key.isReadable()) {
// 7. 处理读事件
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = clientChannel.read(buffer);
if (bytesRead > 0) {
buffer.flip();
byte[] data = new byte[buffer.limit()];
buffer.get(data);
System.out.println("Received: " + new String(data));
// 简单回写
buffer.rewind();
clientChannel.write(buffer);
} else if (bytesRead == -1) {
// 客户端关闭连接
clientChannel.close();
}
}
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# 客户端示例
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NIOClient {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(true); // 阻塞模式也可
socketChannel.connect(new InetSocketAddress("localhost", 8080));
String message = "Hello NIO Server!";
ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
socketChannel.write(buffer);
// 读取回显
buffer.clear();
int bytesRead = socketChannel.read(buffer);
if (bytesRead > 0) {
buffer.flip();
byte[] data = new byte[buffer.limit()];
buffer.get(data);
System.out.println("Echo from server: " + new String(data));
}
socketChannel.close();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
← BIO