NIO
字数: 0 字 时长: 0 分钟
1. NIO 与传统 IO
先来看一张传统 IO 和 NIO 的对比图:

传统 IO 基于字节流或字符流进行数据读写,例如:
FileInputStream
FileOutputStream
BufferedReader
BufferedWriter网络通信则主要使用:
Socket
ServerSocketNIO 则主要通过:
Channel
+
Buffer进行数据读写。
网络通信中还提供了:
SocketChannel
ServerSocketChannel
Selector需要注意:
NIO 的全称是 New I/O。
NIO 提供了非阻塞 IO 的能力,但并不意味着 java.nio 中的所有操作都是非阻塞的。非阻塞和 IO 多路复用的优势,主要体现在网络通信场景中。
简单对比:
| 对比 | 传统 IO | NIO |
|---|---|---|
| 数据处理 | Stream | Channel + Buffer |
| 文件操作 | FileInputStream、FileReader 等 | FileChannel + Buffer |
| 网络通信 | Socket、ServerSocket | SocketChannel、ServerSocketChannel |
| IO 模型 | 主要是阻塞式 | 支持非阻塞 |
| 多连接处理 | 通常一个连接对应一个线程 | Selector 可以管理多个 Channel |
2. NIO 与传统 IO 的文件操作
JDK 1.4 中引入了 java.nio,提供了一套新的 Java IO API。
传统 IO 复制文件时,通常通过输入流和输出流完成:
BufferedInputStream bis =
new BufferedInputStream(
new FileInputStream(source)
);
BufferedOutputStream bos =
new BufferedOutputStream(
new FileOutputStream(des)
);
byte[] bytes =
new byte[1024 * 1024];
int len;
while (
(len = bis.read(bytes)) != -1
) {
bos.write(
bytes,
0,
len
);
}NIO 操作文件时,则主要使用:
FileChannel
+
ByteBuffer例如:
RandomAccessFile read =
new RandomAccessFile(
source,
"rw"
);
RandomAccessFile write =
new RandomAccessFile(
des,
"rw"
);
FileChannel readChannel =
read.getChannel();
FileChannel writeChannel =
write.getChannel();
ByteBuffer byteBuffer =
ByteBuffer.allocate(
1024 * 1024
);
while (
readChannel.read(byteBuffer) > 0
) {
// 切换为读取 Buffer 中数据的状态
byteBuffer.flip();
writeChannel.write(
byteBuffer
);
// 清空 Buffer,继续接收数据
byteBuffer.clear();
}这里涉及两个非常重要的 NIO 组件:
Channel
↓
数据传输通道
Buffer
↓
数据缓冲区RandomAccessFile 是传统 java.io 中的类,而 FileChannel 属于 Java NIO。
与传统流不同,FileChannel 通常配合 ByteBuffer 完成数据传输。
2.1 NIO 操作文件一定更快吗?
不一定。
对于较小的文件:
传统 IO
≈
NIO性能差异通常并不明显。
即使对于大文件,也不能简单认为:
NIO 一定比传统 IO 快实际性能还会受到:
缓冲区大小
磁盘性能
操作系统缓存
JDK 实现
具体读写方式等因素影响。
所以:
NIO 的核心价值并不是“复制文件一定更快”。
文件 IO 只是 NIO 的一个应用场景,它真正明显的优势主要体现在网络 IO 和高并发连接处理中。
3. NIO 在网络 IO 中的优势
IO 操作主要可以分成两个典型场景:
IO
├── 文件 IO
└── 网络 IO而 NIO 的魅力主要体现在:
网络 IO。
传统阻塞式网络 IO 中,通常一个连接需要一个线程进行处理。
大致模型:
客户端 1 ──→ 线程 1
客户端 2 ──→ 线程 2
客户端 3 ──→ 线程 3
客户端 4 ──→ 线程 4
...当连接数量越来越大时,就需要维护大量线程。
随之而来的问题包括:
大量线程创建
大量线程销毁
线程上下文切换
线程资源占用NIO 提供了:
非阻塞 IO
+
IO 多路复用通过 Selector,一个线程可以同时监控多个 Channel。
大致模型:
SocketChannel ──┐
SocketChannel ──┤
SocketChannel ──┼──→ Selector ──→ 一个线程处理事件
SocketChannel ──┤
SocketChannel ──┘因此,在大量网络连接的场景下,NIO 可以减少线程数量,提高并发处理能力。
4. 传统 IO 网络模型
传统网络 IO 主要使用:
ServerSocket
Socket其中:
4.1 ServerSocket
ServerSocket 表示服务器端套接字,主要负责:
监听客户端连接
↓
accept()
↓
创建 Socket
↓
与客户端通信例如:
ServerSocket serverSocket =
new ServerSocket(8080);
while (true) {
// 阻塞等待客户端连接
Socket client =
serverSocket.accept();
InputStream in =
client.getInputStream();
OutputStream out =
client.getOutputStream();
byte[] buffer =
new byte[1024];
int bytesRead =
in.read(buffer);
out.write(
buffer,
0,
bytesRead
);
client.close();
}其中:
serverSocket.accept();默认会阻塞线程,直到有新的客户端建立连接。
读取数据时:
in.read(buffer);同样可能阻塞当前线程。
4.2 Socket
Socket 表示客户端套接字,负责与服务器建立 TCP 连接,并进行数据的读写。
所以传统阻塞式网络 IO 可以简单理解为:
ServerSocket
↓
等待连接
↓
Socket
↓
InputStream / OutputStream
↓
读写数据5. NIO 网络模型
NIO 网络通信主要使用:
ServerSocketChannel
SocketChannel
Selector
ByteBuffer它们之间大致是这样的关系:
Selector
│
┌────────────┼────────────┐
│ │ │
SocketChannel SocketChannel SocketChannel
│ │ │
Buffer Buffer Buffer5.1 ServerSocketChannel
ServerSocketChannel 类似于传统 IO 中的:
ServerSocket用于监听客户端连接。
但它可以设置为非阻塞模式:
ServerSocketChannel serverSocketChannel =
ServerSocketChannel.open();
serverSocketChannel.bind(
new InetSocketAddress(8081)
);
serverSocketChannel.configureBlocking(
false
);5.2 SocketChannel
SocketChannel 类似于传统 IO 中的:
Socket负责:
建立连接
读取数据
写入数据同样可以设置为非阻塞模式:
client.configureBlocking(false);5.3 Selector
Selector 是 Java NIO 中实现 IO 多路复用 的核心组件。
它可以让一个线程同时监控多个:
ServerSocketChannel
SocketChannel例如:
Selector selector =
Selector.open();把 ServerSocketChannel 注册到 Selector:
serverSocketChannel.register(
selector,
SelectionKey.OP_ACCEPT
);表示我们关注:
OP_ACCEPT
↓
新的客户端连接事件当客户端建立连接后,可以再把对应的 SocketChannel 注册到 Selector:
client.register(
selector,
SelectionKey.OP_READ
);表示关注:
OP_READ
↓
数据可读事件因此,整个 NIO 网络模型可以简单理解为:
Channel
↓
注册到
↓
Selector
↓
监听 IO 事件
↓
SelectionKey
↓
处理对应 ChannelSelector、Channel、Buffer 后面分别单独细讲。
6. NIO 服务端基本流程
一个简单的 NIO 服务端基本流程如下:
// 创建 ServerSocketChannel
ServerSocketChannel serverSocketChannel =
ServerSocketChannel.open();
// 绑定端口
serverSocketChannel.bind(
new InetSocketAddress(8081)
);
// 设置为非阻塞模式
serverSocketChannel.configureBlocking(
false
);
// 创建 Selector
Selector selector =
Selector.open();
// 注册连接事件
serverSocketChannel.register(
selector,
SelectionKey.OP_ACCEPT
);
while (true) {
// 等待 IO 事件
selector.select();
Iterator<SelectionKey> iterator =
selector.selectedKeys().iterator();
while (
iterator.hasNext()
) {
SelectionKey key =
iterator.next();
iterator.remove();
if (
key.isAcceptable()
) {
ServerSocketChannel server =
(ServerSocketChannel) key.channel();
SocketChannel client =
server.accept();
client.configureBlocking(
false
);
client.register(
selector,
SelectionKey.OP_READ
);
} else if (
key.isReadable()
) {
SocketChannel client =
(SocketChannel) key.channel();
ByteBuffer buffer =
ByteBuffer.allocate(1024);
int bytesRead =
client.read(buffer);
if (
bytesRead == -1
) {
client.close();
} else {
buffer.flip();
client.write(buffer);
}
}
}
}核心流程可以简化成:
1. 创建 ServerSocketChannel
↓
2. 设置非阻塞
↓
3. 创建 Selector
↓
4. Channel 注册到 Selector
↓
5. selector.select()
↓
6. 获取 SelectionKey
↓
7. 判断事件类型
↓
8. 处理对应 Channel7. NIO 的三个核心组件
学习 Java NIO,最核心的三个组件就是:
NIO
│
├── Buffer
│ └── 数据存储
│
├── Channel
│ └── 数据传输
│
└── Selector
└── IO 多路复用可以先简单理解为:
Buffer
数据放在哪里?
↓
Buffer例如:
ByteBuffer详细内容见:[[Buffer]]
Channel
数据通过什么传输?
↓
Channel常见的 Channel:
FileChannel
SocketChannel
ServerSocketChannel详细内容见:[[Channel]]
Selector
一个线程怎么管理多个网络连接?
↓
SelectorSelector 主要用于:
监听多个 Channel
↓
发现 IO 事件
↓
处理对应连接详细内容见:[[Selector]]
8. 小结
传统 IO 和 NIO 的核心区别,可以先记住:
传统 IO
↓
Stream
↓
Socket / ServerSocket
↓
主要采用阻塞式处理
NIO
↓
Channel + Buffer
↓
SocketChannel / ServerSocketChannel
↓
支持非阻塞模式
↓
Selector
↓
IO 多路复用在文件 IO 场景下:
NIO 不意味着一定比传统 IO 更快。
而在大量网络连接的场景下:
传统 IO
↓
大量连接
↓
大量线程
NIO
↓
多个 Channel
↓
Selector
↓
少量线程管理多个连接因此,NIO 的核心价值主要体现在:
非阻塞网络 IO + IO 多路复用,提高大量并发连接下的处理能力。