Java深入解析篇九之NIO详解
一、NIO概述
1.1 什么是NIO
NIO(New I/O)是Java在JDK 1.4中引入的一套新的I/O API,位于java.nio包下。它提供了面向缓冲区(Buffer)的、非阻塞的I/O操作方式,是对传统BIO(Blocking I/O)的重大改进。JDK 7对NIO进行了进一步完善,引入了NIO.2(java.nio.file和java.nio.channels的增强)。
1.2 NIO与BIO的核心区别
| 特性 | BIO | NIO |
|---|---|---|
| 面向 | 流(Stream) | 缓冲区(Buffer) |
| 阻塞性 | 阻塞 | 非阻塞 |
| 多路复用 | 不支持 | Selector支持 |
| 数据操作 | 单向读取 | 可双向读写 |
| 线程模型 | 一连接一线程 | 一线程多连接 |
1.3 NIO的设计目标
- 高并发:通过Selector实现一个线程管理多个连接
- 高性能:通过零拷贝、内存映射减少数据复制次数
- 灵活性:Buffer可双向操作,支持Scatter/Gather
二、NIO三大核心组件
2.1 核心架构
┌─────────────────────────────────────────────┐
│ Selector │
│ ┌───────────┐ ┌───────────┐ ┌─────────┐ │
│ │ Channel │ │ Channel │ │ Channel │ │
│ │ ↕ │ │ ↕ │ │ ↕ │ │
│ │ Buffer │ │ Buffer │ │ Buffer │ │
│ └───────────┘ └───────────┘ └─────────┘ │
└─────────────────────────────────────────────┘
- Channel(通道):数据传输的通道,类似BIO中的Stream,但Channel是双向的
- Buffer(缓冲区):数据的容器,Channel读写数据都通过Buffer
- Selector(选择器):多路复用器,一个Selector可以监听多个Channel的事件
2.2 三者协作流程
java
// 1. 打开Channel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);
// 2. 创建Selector并注册Channel
Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
// 3. 通过Selector监听事件,使用Buffer读写数据
while (true) {
selector.select(); // 阻塞等待就绪事件
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
if (key.isAcceptable()) {
// 处理连接,使用Buffer读写
}
iter.remove();
}
}
三、Buffer详解
3.1 Buffer核心属性
Buffer有四个核心属性:
| 属性 | 含义 | 说明 |
|---|---|---|
| capacity | 容量 | Buffer最大可存储的数据量,创建后不可变 |
| limit | 界限 | 当前可读写数据的边界 |
| position | 位置 | 下一个读/写操作的索引位置 |
| mark | 标记 | 记录position的临时位置,可reset恢复 |
不变式关系:0 <= mark <= position <= limit <= capacity
3.2 Buffer状态转换
写模式:position从0递增到写入数据量,limit = capacity
↓ flip()
读模式:limit = 原position,position = 0
↓ clear()/compact()
写模式:position = 0,limit = capacity
3.3 核心操作方法
java
import java.nio.ByteBuffer;
public class BufferDemo {
public static void main(String[] args) {
// 创建容量为10的ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(10);
System.out.println("初始状态: " + buffer);
// position=0, limit=10, capacity=10
// 写入数据
buffer.put((byte) 1);
buffer.put((byte) 2);
buffer.put((byte) 3);
System.out.println("写入3字节后: " + buffer);
// position=3, limit=10, capacity=10
// flip():切换为读模式
buffer.flip();
System.out.println("flip后: " + buffer);
// position=0, limit=3, capacity=10
// 读取数据
byte b1 = buffer.get();
byte b2 = buffer.get();
System.out.println("读取: " + b1 + ", " + b2);
// position=2, limit=3
// mark()和reset()
buffer.mark(); // mark = 2
byte b3 = buffer.get(); // position=3
buffer.reset(); // position恢复到2
System.out.println("reset后position: " + buffer.position());
// rewind():position归零,limit不变
buffer.rewind();
System.out.println("rewind后: " + buffer);
// position=0, limit=3
// clear():清空(逻辑清空,数据仍在)
buffer.clear();
System.out.println("clear后: " + buffer);
// position=0, limit=10, capacity=10
// compact():压缩(将未读数据移到开头)
buffer.put((byte) 10);
buffer.put((byte) 20);
buffer.put((byte) 30);
buffer.flip();
buffer.get(); // 读取一个
buffer.compact(); // 未读的20,30移到开头
System.out.println("compact后: " + buffer);
// position=2, limit=10, capacity=10
}
}
3.4 Buffer类型体系
Buffer(抽象基类)
├── ByteBuffer
│ ├── HeapByteBuffer(堆内)
│ └── DirectByteBuffer(堆外)
├── CharBuffer
├── ShortBuffer
├── IntBuffer
├── LongBuffer
├── FloatBuffer
└── DoubleBuffer
3.5 只读Buffer与切片
java
ByteBuffer buffer = ByteBuffer.allocate(16);
buffer.putInt(100).putInt(200).putInt(300);
buffer.flip();
// 只读Buffer
ByteBuffer readOnly = buffer.asReadOnlyBuffer();
// readOnly.put((byte)1); // 抛出ReadOnlyBufferException
// 切片(共享底层数据)
buffer.position(4);
buffer.limit(12);
ByteBuffer slice = buffer.slice();
// slice与buffer共享position 4~11的数据
// 复制(独立数据)
ByteBuffer duplicate = buffer.duplicate();
四、ByteBuffer:HeapBuffer vs DirectBuffer
4.1 HeapByteBuffer(堆内缓冲区)
java
// 分配在JVM堆内存中
ByteBuffer heapBuffer = ByteBuffer.allocate(1024);
// 特点:
// - 分配和回收速度快(受GC管理)
// - 进行I/O操作时需要复制到堆外(JNI调用)
// - 适合小数据量、频繁创建销毁的场景
4.2 DirectByteBuffer(堆外缓冲区)
java
// 分配在操作系统本地内存中
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);
// 特点:
// - 分配和回收速度慢(系统调用malloc)
// - I/O操作无需额外复制(直接传递给OS)
// - 不受GC直接管理,通过Cleaner机制回收
// - 适合大数据量、长期使用的I/O场景
4.3 性能对比与选择
java
public class BufferBenchmark {
public static void main(String[] args) throws Exception {
int size = 1024 * 1024; // 1MB
int iterations = 1000;
// HeapBuffer测试
ByteBuffer heap = ByteBuffer.allocate(size);
long start = System.nanoTime();
for (int i = 0; i < iterations; i++) {
heap.clear();
// 模拟写入
for (int j = 0; j < size / 8; j++) {
heap.putLong(j * 8, j);
}
}
System.out.println("HeapBuffer: " + (System.nanoTime() - start) / 1_000_000 + "ms");
// DirectBuffer测试
ByteBuffer direct = ByteBuffer.allocateDirect(size);
start = System.nanoTime();
for (int i = 0; i < iterations; i++) {
direct.clear();
for (int j = 0; j < size / 8; j++) {
direct.putLong(j * 8, j);
}
}
System.out.println("DirectBuffer: " + (System.nanoTime() - start) / 1_000_000 + "ms");
}
}
4.4 DirectBuffer的内存管理
java
// DirectBuffer通过sun.misc.Cleaner(JDK8)或jdk.internal.ref.Cleaner(JDK9+)回收
// 如果DirectBuffer对象被GC回收,其对应的本地内存也会被释放
// 手动释放(不推荐,依赖内部API):
// ((sun.nio.ch.DirectBuffer) directBuffer).cleaner().clean();
// JVM参数控制直接内存大小:
// -XX:MaxDirectMemorySize=256m
// 最佳实践:复用DirectBuffer,避免频繁创建
五、Channel体系
5.1 Channel概述
Channel是NIO中数据传输的通道,与Stream的区别:
- Channel是双向的(可读可写),Stream是单向的
- Channel可以异步读写
- Channel读写必须通过Buffer
5.2 FileChannel
java
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelDemo {
public static void main(String[] args) throws Exception {
// 写入文件
try (FileChannel writeChannel = new RandomAccessFile("output.txt", "rw").getChannel()) {
ByteBuffer buffer = ByteBuffer.allocate(256);
buffer.put("Hello, Java NIO!".getBytes());
buffer.flip();
while (buffer.hasRemaining()) {
writeChannel.write(buffer);
}
}
// 读取文件
try (FileChannel readChannel = new RandomAccessFile("output.txt", "r").getChannel()) {
ByteBuffer buffer = ByteBuffer.allocate(256);
int bytesRead;
StringBuilder sb = new StringBuilder();
while ((bytesRead = readChannel.read(buffer)) != -1) {
buffer.flip();
while (buffer.hasRemaining()) {
sb.append((char) buffer.get());
}
buffer.clear();
}
System.out.println("文件内容: " + sb);
}
}
}
5.3 SocketChannel(TCP客户端)
java
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NioClient {
public static void main(String[] args) throws Exception {
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
channel.connect(new InetSocketAddress("localhost", 8080));
// 非阻塞连接需要等待连接完成
while (!channel.finishConnect()) {
// 可以做其他事情
Thread.sleep(10);
}
System.out.println("连接成功");
// 发送数据
ByteBuffer buffer = ByteBuffer.allocate(256);
buffer.put("Hello Server".getBytes());
buffer.flip();
while (buffer.hasRemaining()) {
channel.write(buffer);
}
// 接收数据
buffer.clear();
int bytesRead;
while ((bytesRead = channel.read(buffer)) > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
buffer.clear();
}
channel.close();
}
}
5.4 ServerSocketChannel(TCP服务端)
java
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class NioServer {
public static void main(String[] args) throws Exception {
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);
System.out.println("服务器启动,端口8080");
while (true) {
// 非阻塞accept,无连接时返回null
SocketChannel clientChannel = serverChannel.accept();
if (clientChannel != null) {
System.out.println("新连接: " + clientChannel.getRemoteAddress());
handleClient(clientChannel);
}
Thread.sleep(100); // 避免忙等
}
}
private static void handleClient(SocketChannel channel) throws Exception {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer);
if (bytesRead > 0) {
buffer.flip();
byte[] data = new byte[bytesRead];
buffer.get(data);
System.out.println("收到: " + new String(data));
// 回写
buffer.clear();
buffer.put(("Echo: " + new String(data)).getBytes());
buffer.flip();
channel.write(buffer);
}
channel.close();
}
}
5.5 DatagramChannel(UDP)
java
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
public class UdpDemo {
public static void main(String[] args) throws Exception {
// UDP服务端
DatagramChannel server = DatagramChannel.open();
server.bind(new InetSocketAddress(9090));
server.configureBlocking(false);
// UDP客户端
DatagramChannel client = DatagramChannel.open();
ByteBuffer buffer = ByteBuffer.allocate(256);
buffer.put("UDP Message".getBytes());
buffer.flip();
client.send(buffer, new InetSocketAddress("localhost", 9090));
// 服务端接收
buffer.clear();
java.net.SocketAddress sender = server.receive(buffer);
if (sender != null) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
System.out.println("收到来自 " + sender + ": " + new String(data));
}
client.close();
server.close();
}
}
六、Selector多路复用器
6.1 多路复用原理
Selector底层依赖操作系统的I/O多路复用机制:
| 机制 | 平台 | 特点 |
|---|---|---|
| select | 所有 | fd数量限制1024,每次需遍历所有fd,O(n) |
| poll | Linux/Unix | 无fd数量限制,仍需遍历,O(n) |
| epoll | Linux | 事件驱动,O(1)就绪通知,支持ET/LT模式 |
| kqueue | macOS/BSD | 类似epoll的事件通知机制 |
Java NIO在Linux上默认使用epoll(JDK 1.5+),Windows上使用select。
6.2 Selector基本用法
java
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
public class SelectorDemo {
public static void main(String[] args) throws Exception {
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new java.net.InetSocketAddress(8080));
serverChannel.configureBlocking(false);
// 注册Channel到Selector
SelectionKey key = serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// select():阻塞直到有事件就绪
// select(timeout):阻塞最多timeout毫秒
// selectNow():非阻塞,立即返回
int readyCount = selector.select();
if (readyCount == 0) continue;
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectedKey = iterator.next();
if (selectedKey.isAcceptable()) {
handleAccept(selectedKey);
} else if (selectedKey.isReadable()) {
handleRead(selectedKey);
} else if (selectedKey.isWritable()) {
handleWrite(selectedKey);
}
// 必须手动移除,否则下次select还会返回
iterator.remove();
}
}
}
private static void handleAccept(SelectionKey key) throws Exception {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(key.selector(), SelectionKey.OP_READ);
}
private static void handleRead(SelectionKey key) throws Exception {
SocketChannel channel = (SocketChannel) key.channel();
java.nio.ByteBuffer buffer = java.nio.ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer);
if (bytesRead == -1) {
channel.close();
return;
}
buffer.flip();
// 处理数据...
key.interestOps(SelectionKey.OP_WRITE); // 切换为写事件
}
private static void handleWrite(SelectionKey key) throws Exception {
SocketChannel channel = (SocketChannel) key.channel();
java.nio.ByteBuffer buffer = java.nio.ByteBuffer.wrap("Response".getBytes());
channel.write(buffer);
key.interestOps(SelectionKey.OP_READ); // 切回读事件
}
}
6.3 Selector的wakeup机制
java
// 当一个线程阻塞在select()上时,另一个线程可以调用wakeup()使其立即返回
Selector selector = Selector.open();
// 线程A:阻塞等待
new Thread(() -> {
try {
selector.select(); // 阻塞
System.out.println("select返回");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
// 线程B:唤醒
new Thread(() -> {
try {
Thread.sleep(3000);
selector.wakeup(); // 使select()立即返回
} catch (Exception e) {
e.printStackTrace();
}
}).start();
七、SelectionKey与事件注册
7.1 SelectionKey核心概念
SelectionKey表示Channel与Selector之间的注册关系,包含:
java
// 四种事件类型
SelectionKey.OP_ACCEPT = 1 << 0; // 1 服务端接受连接
SelectionKey.OP_CONNECT = 1 << 1; // 2 客户端连接完成
SelectionKey.OP_READ = 1 << 2; // 4 通道可读
SelectionKey.OP_WRITE = 1 << 3; // 8 通道可写
// SelectionKey的核心方法
key.channel(); // 获取关联的Channel
key.selector(); // 获取关联的Selector
key.interestOps(); // 获取感兴趣的事件集合
key.interestOps(int); // 修改感兴趣的事件
key.readyOps(); // 获取已就绪的事件
key.isValid(); // 是否有效
key.cancel(); // 取消注册
key.attach(Object); // 附加对象(如Buffer、业务上下文)
key.attachment(); // 获取附加对象
7.2 使用attachment传递上下文
java
// 注册时附加Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, buffer);
// 事件处理时获取
public void handleRead(SelectionKey key) throws Exception {
ByteBuffer buffer = (ByteBuffer) key.attachment();
SocketChannel channel = (SocketChannel) key.channel();
buffer.clear();
int bytesRead = channel.read(buffer);
if (bytesRead > 0) {
buffer.flip();
// 处理数据
}
}
7.3 事件组合与切换
java
// 注册多个事件
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
// 动态切换事件
if (writeBuffer.hasRemaining()) {
key.interestOps(SelectionKey.OP_WRITE); // 数据未写完,关注写事件
} else {
key.interestOps(SelectionKey.OP_READ); // 写完了,切回读事件
}
// 判断就绪事件
if (key.isReadable()) { /* ... */ }
if (key.isWritable()) { /* ... */ }
if (key.isAcceptable()) { /* ... */ }
if (key.isConnectable()) { /* ... */ }
八、NIO网络编程(完整非阻塞示例)
8.1 非阻塞NIO服务器
java
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 NioReactorServer {
private Selector selector;
private ServerSocketChannel serverChannel;
public NioReactorServer(int port) throws IOException {
selector = Selector.open();
serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(port));
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO Server started on port " + port);
}
public void start() throws IOException {
while (true) {
int readyCount = selector.select(1000);
if (readyCount == 0) continue;
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
try {
if (!key.isValid()) continue;
if (key.isAcceptable()) {
doAccept(key);
} else if (key.isReadable()) {
doRead(key);
} else if (key.isWritable()) {
doWrite(key);
}
} catch (IOException e) {
key.cancel();
key.channel().close();
}
}
}
}
private void doAccept(SelectionKey key) throws IOException {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
if (client == null) return;
client.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.register(selector, SelectionKey.OP_READ, buffer);
System.out.println("Client connected: " + client.getRemoteAddress());
}
private void doRead(SelectionKey key) throws IOException {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.clear();
int bytesRead = client.read(buffer);
if (bytesRead == -1) {
System.out.println("Client disconnected: " + client.getRemoteAddress());
client.close();
key.cancel();
return;
}
if (bytesRead > 0) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String message = new String(data).trim();
System.out.println("Received: " + message);
// 准备回写数据
String response = "Server Echo: " + message + "\n";
ByteBuffer writeBuffer = ByteBuffer.wrap(response.getBytes());
key.attach(writeBuffer);
key.interestOps(SelectionKey.OP_WRITE);
}
}
private void doWrite(SelectionKey key) throws IOException {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
while (buffer.hasRemaining()) {
if (client.write(buffer) == 0) break;
}
if (!buffer.hasRemaining()) {
// 写完了,切回读事件,重新附加读Buffer
key.attach(ByteBuffer.allocate(1024));
key.interestOps(SelectionKey.OP_READ);
}
}
public static void main(String[] args) throws IOException {
new NioReactorServer(8080).start();
}
}
8.2 非阻塞NIO客户端
java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;
public class NioReactorClient {
private Selector selector;
private SocketChannel channel;
public NioReactorClient(String host, int port) throws IOException {
selector = Selector.open();
channel = SocketChannel.open();
channel.configureBlocking(false);
channel.connect(new InetSocketAddress(host, port));
channel.register(selector, SelectionKey.OP_CONNECT);
}
public void start() throws IOException {
// 输入线程
new Thread(() -> {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String msg = scanner.nextLine();
try {
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
while (buffer.hasRemaining()) {
channel.write(buffer);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
// 事件循环
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.isConnectable()) {
SocketChannel ch = (SocketChannel) key.channel();
if (ch.finishConnect()) {
System.out.println("Connected to server");
key.interestOps(SelectionKey.OP_READ);
}
} else 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("Server: " + new String(buffer.array(), 0, bytesRead));
} else if (bytesRead == -1) {
ch.close();
return;
}
}
}
}
}
public static void main(String[] args) throws IOException {
new NioReactorClient("localhost", 8080).start();
}
}
九、零拷贝技术
9.1 传统I/O的数据拷贝
传统文件传输(read + write)需要4次拷贝 + 4次上下文切换:
磁盘 → 内核缓冲区 → 用户空间Buffer → Socket缓冲区 → 网卡
(DMA) (CPU) (CPU) (DMA)
9.2 transferTo(sendfile)
java
import java.io.RandomAccessFile;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
public class ZeroCopySend {
public static void main(String[] args) throws Exception {
// 使用transferTo实现零拷贝文件传输
try (FileChannel fileChannel = new RandomAccessFile("large-file.dat", "r").getChannel();
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 8080))) {
long fileSize = fileChannel.size();
long transferred = 0;
// 数据直接从文件描述符传输到Socket,不经过用户空间
while (transferred < fileSize) {
transferred += fileChannel.transferTo(transferred, fileSize - transferred, socketChannel);
}
System.out.println("传输完成: " + transferred + " bytes");
}
}
}
9.3 transferFrom
java
import java.io.RandomAccessFile;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
public class ZeroCopyReceive {
public static void main(String[] args) throws Exception {
try (FileChannel fileChannel = new RandomAccessFile("received.dat", "rw").getChannel();
SocketChannel socketChannel = SocketChannel.open()) {
socketChannel.bind(new InetSocketAddress(8080));
// 实际场景中通过ServerSocketChannel.accept()获取
long position = 0;
long count = 1024 * 1024; // 1MB
long transferred;
// 从Socket直接写入文件,不经过用户空间
while ((transferred = fileChannel.transferFrom(socketChannel, position, count)) > 0) {
position += transferred;
}
System.out.println("接收完成: " + position + " bytes");
}
}
}
9.4 mmap内存映射(MappedByteBuffer)
java
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
public class MmapDemo {
public static void main(String[] args) throws Exception {
try (RandomAccessFile raf = new RandomAccessFile("mmap-test.dat", "rw");
FileChannel channel = raf.getChannel()) {
long fileSize = 1024 * 1024; // 1MB
raf.setLength(fileSize);
// 创建内存映射
MappedByteBuffer mappedBuffer = channel.map(
FileChannel.MapMode.READ_WRITE, 0, fileSize);
// 像操作内存一样操作文件
mappedBuffer.putInt(0, 42);
mappedBuffer.putLong(4, System.currentTimeMillis());
// 读取
int value = mappedBuffer.getInt(0);
long timestamp = mappedBuffer.getLong(4);
System.out.println("Value: " + value + ", Time: " + timestamp);
// 强制刷盘
mappedBuffer.force();
// 注意:MappedByteBuffer没有显式close方法
// 依赖GC回收,或通过反射调用Cleaner
}
}
}
9.5 零拷贝对比
| 方式 | 拷贝次数 | 上下文切换 | 适用场景 |
|---|---|---|---|
| 传统read+write | 4次 | 4次 | 通用 |
| transferTo (sendfile) | 2次(DMA) | 2次 | 文件→网络 |
| transferTo + DMA gather | 0次CPU拷贝 | 2次 | 文件→网络(Linux 2.4+) |
| mmap | 1次(DMA) | 4次 | 文件读写、随机访问 |
十、MappedByteBuffer内存映射文件
10.1 三种映射模式
java
FileChannel.MapMode.READ_ONLY // 只读,尝试写入抛NonWritableChannelException
FileChannel.MapMode.READ_WRITE // 读写,修改会写回文件
FileChannel.MapMode.PRIVATE // 写时复制,修改不影响原文件
10.2 大文件处理
java
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
public class LargeFileProcessor {
private static final long CHUNK_SIZE = Integer.MAX_VALUE; // 单次映射最大约2GB
public static void processLargeFile(String filePath) throws Exception {
try (RandomAccessFile raf = new RandomAccessFile(filePath, "r");
FileChannel channel = raf.getChannel()) {
long fileSize = channel.size();
long position = 0;
while (position < fileSize) {
long remaining = fileSize - position;
long mapSize = Math.min(remaining, CHUNK_SIZE);
MappedByteBuffer buffer = channel.map(
FileChannel.MapMode.READ_ONLY, position, mapSize);
// 处理当前块
processChunk(buffer);
position += mapSize;
// buffer由GC回收释放映射
}
}
}
private static void processChunk(MappedByteBuffer buffer) {
// 处理数据块
while (buffer.hasRemaining()) {
byte b = buffer.get();
// 业务逻辑...
}
}
}
10.3 MappedByteBuffer的注意事项
java
// 1. 映射后文件被删除/截断,访问会抛IOException
// 2. 没有显式unmap方法,依赖GC(可能导致内存泄漏)
// 3. JDK9+可使用sun.misc.Unsafe或Cleaner手动释放:
// JDK9+ 释放方式(不推荐生产使用):
// sun.misc.Unsafe unsafe = ...;
// unsafe.invokeCleaner(mappedByteBuffer);
// 4. 映射区域大小受限于Integer.MAX_VALUE(约2GB)
// 5. 频繁创建小映射性能差,应复用或批量映射
十一、Scatter/Gather操作
11.1 Scatter(分散读取)
将Channel中的数据分散到多个Buffer中:
java
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class ScatterDemo {
public static void main(String[] args) throws Exception {
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
SocketChannel client = server.accept();
// 定义协议:前4字节为消息长度,接下来为消息头,最后为消息体
ByteBuffer headerBuffer = ByteBuffer.allocate(4);
ByteBuffer bodyBuffer = ByteBuffer.allocate(1024);
ByteBuffer[] buffers = {headerBuffer, bodyBuffer};
// Scatter读取:先填满headerBuffer,再填bodyBuffer
long bytesRead = client.read(buffers);
headerBuffer.flip();
int msgLength = headerBuffer.getInt();
System.out.println("消息长度: " + msgLength);
bodyBuffer.flip();
byte[] body = new byte[bodyBuffer.remaining()];
bodyBuffer.get(body);
System.out.println("消息体: " + new String(body));
client.close();
server.close();
}
}
11.2 Gather(聚集写入)
将多个Buffer中的数据聚集写入Channel:
java
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class GatherDemo {
public static void main(String[] args) throws Exception {
SocketChannel channel = SocketChannel.open(new InetSocketAddress("localhost", 8080));
// 构造响应:状态码 + 消息头 + 消息体
ByteBuffer statusBuffer = ByteBuffer.allocate(4);
statusBuffer.putInt(200);
statusBuffer.flip();
ByteBuffer headerBuffer = ByteBuffer.wrap("Content-Type: text/plain\r\n".getBytes());
ByteBuffer bodyBuffer = ByteBuffer.wrap("Hello, Scatter/Gather!".getBytes());
ByteBuffer[] buffers = {statusBuffer, headerBuffer, bodyBuffer};
// Gather写入:按顺序将多个Buffer写入Channel
long bytesWritten = channel.write(buffers);
System.out.println("写入字节数: " + bytesWritten);
channel.close();
}
}
11.3 带偏移量的Scatter/Gather
java
// 只操作Buffer数组的一部分
ByteBuffer[] buffers = new ByteBuffer[5];
// ... 初始化各buffer
// 从第2个buffer开始,操作3个buffer
long bytesRead = channel.read(buffers, 1, 3);
long bytesWritten = channel.write(buffers, 1, 3);
十二、FileChannel文件锁
12.1 文件锁概述
文件锁用于多进程/多线程间对文件的并发访问控制:
- 排他锁(Exclusive Lock):写锁,其他进程不能读写
- 共享锁(Shared Lock):读锁,其他进程可以读但不能写
12.2 文件锁使用示例
java
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
public class FileLockDemo {
public static void main(String[] args) throws Exception {
// 排他锁(写锁)
try (RandomAccessFile raf = new RandomAccessFile("locked-file.txt", "rw");
FileChannel channel = raf.getChannel()) {
// 获取整个文件的排他锁(阻塞)
FileLock lock = channel.lock();
try {
System.out.println("获得排他锁,开始写入...");
channel.write(java.nio.ByteBuffer.wrap("Locked data".getBytes()));
Thread.sleep(5000); // 模拟长时间操作
} finally {
lock.release();
System.out.println("释放排他锁");
}
}
// 共享锁(读锁)
try (RandomAccessFile raf = new RandomAccessFile("locked-file.txt", "r");
FileChannel channel = raf.getChannel()) {
// 获取共享锁
FileLock sharedLock = channel.lock(0, Long.MAX_VALUE, true);
try {
System.out.println("获得共享锁,开始读取...");
java.nio.ByteBuffer buffer = java.nio.ByteBuffer.allocate(256);
channel.read(buffer);
buffer.flip();
// 读取数据...
} finally {
sharedLock.release();
}
}
}
}
12.3 非阻塞锁与区域锁
java
// 非阻塞尝试获取锁
FileLock lock = channel.tryLock();
if (lock == null) {
System.out.println("无法获取锁,其他进程正在使用");
} else {
try {
// 操作文件
} finally {
lock.release();
}
}
// 区域锁:只锁定文件的一部分
// 参数:position, size, shared
FileLock regionLock = channel.lock(0, 1024, false); // 锁定前1024字节
// 检查锁是否有效
boolean valid = lock.isValid();
// 检查是否为共享锁
boolean isShared = lock.isShared();
12.4 文件锁注意事项
- 文件锁是进程级别的(JVM级别),同一JVM内多线程不互斥
- 在Windows上,文件被锁定时不能删除
- 锁的粒度是文件区域,不同区域可以分别加锁
FileLock实现了AutoCloseable,可用try-with-resources(JDK 7+)- 网络文件系统(NFS)上文件锁可能不可靠
十三、Reactor模式
13.1 单Reactor单线程模型
┌────────────────────────────────────┐
│ Reactor线程 │
│ ┌──────────┐ ┌──────────────┐ │
│ │ Selector │───→│ 事件分发器 │ │
│ │ (select) │ │ (dispatch) │ │
│ └──────────┘ └──────┬───────┘ │
│ │ │
│ ┌──────────┼────────┐ │
│ ↓ ↓ ↓ │
│ [Accept] [Read] [Write]│
│ Handler Handler Handler│
└────────────────────────────────────┘
所有I/O操作和业务处理在同一线程,适合低并发场景。
13.2 单Reactor多线程模型
┌──────────────────────────────────────────┐
│ Reactor线程 │
│ Selector → Dispatch → Handler │
└──────────────────┬───────────────────────┘
│ 业务处理交给线程池
↓
┌──────────────────────────────────────────┐
│ Worker Thread Pool │
│ [Thread-1] [Thread-2] [Thread-3] ... │
└──────────────────────────────────────────┘
13.3 主从Reactor多线程模型(Netty采用)
┌─────────────────────┐
│ MainReactor │ ← 只负责Accept
│ (Boss Group) │
│ Selector │
└─────────┬───────────┘
│ 新连接分发
↓
┌─────────────────────────────────────────┐
│ SubReactor Pool │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │SubReactor│ │SubReactor│ │SubReactor│ │ ← 负责Read/Write
│ │(Worker-1)│ │(Worker-2)│ │(Worker-3)│ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
└───────┼────────────┼────────────┼────────┘
↓ ↓ ↓
┌─────────────────────────────────────────┐
│ Business Thread Pool │ ← 业务逻辑处理
└─────────────────────────────────────────┘
13.4 主从Reactor代码实现
java
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.ExecutorService;
import java.util.concurrent.Executors;
public class MainSubReactorServer {
private final int port;
private final ExecutorService bossPool = Executors.newSingleThreadExecutor();
private final ExecutorService workerPool = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors());
public MainSubReactorServer(int port) {
this.port = port;
}
public void start() throws IOException {
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(port));
serverChannel.configureBlocking(false);
Selector bossSelector = Selector.open();
serverChannel.register(bossSelector, SelectionKey.OP_ACCEPT);
System.out.println("Server started on port " + port);
// Boss线程:只处理Accept
bossPool.submit(() -> {
try {
while (true) {
bossSelector.select();
Set<SelectionKey> keys = bossSelector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isAcceptable()) {
SocketChannel client = serverChannel.accept();
if (client != null) {
client.configureBlocking(false);
// 将新连接分配给Worker
workerPool.submit(new WorkerHandler(client));
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
});
}
// Worker:处理已建立连接的I/O
static class WorkerHandler implements Runnable {
private final SocketChannel channel;
private final Selector selector;
WorkerHandler(SocketChannel channel) throws IOException {
this.channel = channel;
this.selector = Selector.open();
channel.register(selector, SelectionKey.OP_READ);
}
@Override
public void run() {
try {
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.isReadable()) {
handleRead(key);
}
}
}
} catch (IOException e) {
try { channel.close(); } catch (IOException ignored) {}
}
}
private void handleRead(SelectionKey key) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer);
if (bytesRead == -1) {
channel.close();
return;
}
buffer.flip();
// 处理业务逻辑并回写
channel.write(buffer);
}
}
public static void main(String[] args) throws IOException {
new MainSubReactorServer(8080).start();
}
}
十四、NIO vs BIO性能对比
14.1 并发连接数对比
java
// BIO服务器:每个连接一个线程
public class BioServer {
public static void main(String[] args) throws Exception {
ServerSocket server = new ServerSocket(8080);
ExecutorService pool = Executors.newFixedThreadPool(200); // 线程数受限
while (true) {
Socket socket = server.accept(); // 阻塞
pool.submit(() -> {
try {
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) != -1) { // 阻塞读
out.write(buf, 0, len);
out.flush();
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
}
14.2 性能指标对比
| 指标 | BIO | NIO |
|---|---|---|
| 1000并发连接 | 需1000线程,内存约1GB | 1-2个线程即可 |
| 线程切换开销 | 高(上下文切换频繁) | 极低 |
| 连接空闲时 | 线程阻塞等待,浪费资源 | Selector统一管理 |
| 吞吐量 | 受线程数限制 | 受CPU和带宽限制 |
| 编程复杂度 | 简单直观 | 较复杂(状态管理) |
| 适用场景 | 连接数少、数据量大 | 连接数多、短请求 |
14.3 选择建议
连接数 < 1000 且 请求数据量大 → BIO(简单可靠)
连接数 > 1000 且 请求频繁短小 → NIO(高并发)
需要文件I/O高性能 → NIO(零拷贝、mmap)
需要跨平台简单I/O → BIO(兼容性好)
十五、Netty对NIO的封装简介
15.1 Netty解决的问题
原生NIO的痛点:
- API复杂,开发效率低
- 粘包/拆包需要自行处理
- 断线重连、心跳检测需手动实现
- Selector空轮询Bug(epoll bug导致CPU 100%)
- 内存管理复杂(DirectBuffer泄漏)
15.2 Netty架构概览
┌─────────────────────────────────────────┐
│ Netty Application │
├─────────────────────────────────────────┤
│ ChannelHandler (业务逻辑) │
├─────────────────────────────────────────┤
│ ChannelPipeline (责任链) │
├─────────────────────────────────────────┤
│ Codec (编解码:粘包/拆包/协议) │
├─────────────────────────────────────────┤
│ EventLoop (线程模型:主从Reactor) │
├─────────────────────────────────────────┤
│ Bootstrap (启动引导) │
├─────────────────────────────────────────┤
│ Java NIO / Epoll / KQueue │
└─────────────────────────────────────────┘
15.3 Netty服务端示例
java
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
public class NettyServer {
public static void main(String[] args) throws Exception {
// Boss线程组:处理Accept
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// Worker线程组:处理I/O
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringDecoder());
pipeline.addLast(new StringEncoder());
pipeline.addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
System.out.println("Received: " + msg);
ctx.writeAndFlush("Echo: " + msg + "\n");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
});
}
});
ChannelFuture future = bootstrap.bind(8080).sync();
System.out.println("Netty Server started on port 8080");
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
15.4 Netty核心组件映射
| Netty组件 | 对应NIO概念 | 增强 |
|---|---|---|
| EventLoopGroup | Selector + Thread | 线程池管理 |
| Channel | SocketChannel | 统一抽象 |
| ChannelPipeline | - | 责任链处理 |
| ByteBuf | ByteBuffer | 读写分离、池化 |
| ChannelFuture | - | 异步回调 |
| Bootstrap | - | 流式配置 |
十六、最佳实践与常见陷阱
16.1 Buffer使用最佳实践
java
// 1. 始终检查write返回值(可能写入0字节)
int written = channel.write(buffer);
if (written == 0) {
// Socket发送缓冲区满,注册OP_WRITE等待可写
key.interestOps(SelectionKey.OP_WRITE);
}
// 2. 使用Buffer池避免频繁分配
public class BufferPool {
private static final ThreadLocal<ByteBuffer> BUFFER_POOL =
ThreadLocal.withInitial(() -> ByteBuffer.allocateDirect(4096));
public static ByteBuffer getBuffer() {
ByteBuffer buffer = BUFFER_POOL.get();
buffer.clear();
return buffer;
}
}
// 3. 批量写入减少系统调用
ByteBuffer[] buffers = {header, body};
while (true) {
long written = channel.write(buffers);
if (written == 0) break;
if (!hasRemaining(buffers)) break;
}
16.2 Selector使用陷阱
java
// 陷阱1:必须手动remove已处理的SelectionKey
// 否则下次select()还会返回相同的key
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove(); // 必须!
// 处理key...
}
// 陷阱2:Selector空轮询Bug(JDK epoll bug)
// 现象:select()在没有就绪事件时立即返回,导致CPU 100%
// 解决方案(Netty的做法):
int emptySelectCount = 0;
while (true) {
long beforeSelect = System.nanoTime();
int count = selector.select(1000);
long selectTime = System.nanoTime() - beforeSelect;
if (count == 0 && selectTime < 500_000) { // 小于0.5ms就返回
emptySelectCount++;
if (emptySelectCount >= 512) {
// 重建Selector
Selector newSelector = Selector.open();
for (SelectionKey key : selector.keys()) {
key.channel().register(newSelector, key.interestOps(), key.attachment());
}
selector.close();
selector = newSelector;
emptySelectCount = 0;
}
} else {
emptySelectCount = 0;
}
}
// 陷阱3:在select()之前注册新Channel
// 正确做法:使用wakeup() + 队列
private final Queue<Runnable> pendingRegistrations = new ConcurrentLinkedQueue<>();
public void register(SocketChannel channel, int ops) {
pendingRegistrations.add(() -> {
try {
channel.register(selector, ops);
} catch (ClosedChannelException e) {
e.printStackTrace();
}
});
selector.wakeup();
}
// 在事件循环中处理
while (true) {
selector.select(1000);
Runnable task;
while ((task = pendingRegistrations.poll()) != null) {
task.run();
}
// 处理就绪事件...
}
16.3 Channel使用注意事项
java
// 1. 非阻塞模式下read可能返回0(无数据可读)
int bytesRead = channel.read(buffer);
if (bytesRead == 0) {
// 正常情况,不是错误
return;
}
if (bytesRead == -1) {
// 对端关闭连接
channel.close();
}
// 2. 非阻塞connect需要finishConnect
channel.configureBlocking(false);
channel.connect(address);
// 不能立即读写,需要等待连接完成
while (!channel.finishConnect()) {
// 等待或做其他事
}
// 3. 关闭Channel时注意顺序
// 先取消SelectionKey,再关闭Channel
key.cancel();
channel.close();
selector.wakeup(); // 确保cancel生效
// 4. FileChannel是阻塞的(不能设为非阻塞)
// 但可以通过线程池实现异步文件I/O
16.4 内存管理最佳实践
java
// 1. DirectBuffer要复用,不要频繁创建
// 错误示范:
void handleRequest() {
ByteBuffer buf = ByteBuffer.allocateDirect(4096); // 每次请求都分配!
// ...
}
// 正确示范:使用池化
// Netty的PooledByteBufAllocator就是解决方案
// 2. 设置合理的MaxDirectMemorySize
// -XX:MaxDirectMemorySize=512m
// 默认等于-Xmx,但容器环境可能不准确
// 3. 监控直接内存使用
// JMX: java.nio:type=BufferPool,name=direct
// 或 -XX:NativeMemoryTracking=detail + jcmd
// 4. 避免MappedByteBuffer泄漏
// 映射后不要持有引用过久
// 大文件分块映射处理
16.5 常见异常与解决
| 异常 | 原因 | 解决方案 |
|---|---|---|
| BufferOverflowException | 写入超过limit | 检查capacity,扩容或分批写 |
| BufferUnderflowException | 读取超过limit | 检查remaining() |
| ReadOnlyBufferException | 写入只读Buffer | 使用duplicate()或asReadOnlyBuffer()前检查 |
| ClosedChannelException | Channel已关闭 | 检查isOpen(),处理并发关闭 |
| CancelledKeyException | SelectionKey已取消 | 检查isValid() |
| NonWritableChannelException | 以只读模式打开 | 检查打开模式 |
| OutOfMemoryError: Direct buffer memory | 直接内存不足 | 增大MaxDirectMemorySize或修复泄漏 |
16.6 生产环境检查清单
□ 所有Channel和Selector在finally/try-with-resources中关闭
□ SelectionKey在迭代器中手动remove
□ 处理read返回0和-1的情况
□ 非阻塞connect后调用finishConnect
□ write返回0时注册OP_WRITE而非忙等
□ DirectBuffer复用或使用池化
□ 设置合理的Socket缓冲区(SO_RCVBUF/SO_SNDBUF)
□ 处理Selector空轮询Bug(重建Selector)
□ 新Channel注册通过wakeup+队列,避免并发问题
□ 设置TCP_NODELAY减少延迟(小消息场景)
□ 设置SO_KEEPALIVE检测死连接
□ 业务逻辑不在I/O线程执行(交给业务线程池)
□ 设置读写超时(通过ScheduledExecutor定时检测)
□ 监控Selector管理的连接数和就绪事件频率
十七、NIO.2补充(JDK 7+)
17.1 Path与Files工具类
java
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
public class Nio2Demo {
public static void main(String[] args) throws Exception {
// Path操作
Path path = Paths.get("E:/data", "test.txt");
System.out.println("文件名: " + path.getFileName());
System.out.println("父目录: " + path.getParent());
System.out.println("绝对路径: " + path.toAbsolutePath());
// Files工具类
// 读取所有行
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
// 写入
Files.write(path, "Hello NIO.2".getBytes(), StandardOpenOption.CREATE);
// 复制
Files.copy(Paths.get("source.txt"), Paths.get("target.txt"),
StandardCopyOption.REPLACE_EXISTING);
// 遍历目录
try (DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get("."), "*.java")) {
for (Path entry : stream) {
System.out.println(entry);
}
}
// WatchService文件监控
WatchService watcher = FileSystems.getDefault().newWatchService();
Paths.get(".").register(watcher,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE);
WatchKey key = watcher.take(); // 阻塞等待事件
for (WatchEvent<?> event : key.pollEvents()) {
System.out.println(event.kind() + ": " + event.context());
}
key.reset();
}
}
17.2 AsynchronousFileChannel(异步文件I/O)
java
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.*;
import java.util.concurrent.Future;
public class AsyncFileDemo {
public static void main(String[] args) throws Exception {
AsynchronousFileChannel channel = AsynchronousFileChannel.open(
Paths.get("async-test.txt"),
StandardOpenOption.READ, StandardOpenOption.WRITE,
StandardOpenOption.CREATE);
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 方式1:Future
Future<Integer> writeFuture = channel.write(
ByteBuffer.wrap("Async write".getBytes()), 0);
int written = writeFuture.get(); // 阻塞等待完成
System.out.println("写入: " + written + " bytes");
// 方式2:CompletionHandler回调
buffer.clear();
channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
attachment.flip();
byte[] data = new byte[attachment.remaining()];
attachment.get(data);
System.out.println("异步读取: " + new String(data));
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
Thread.sleep(1000); // 等待回调完成
channel.close();
}
}
十八、总结
18.1 NIO核心知识体系
Java NIO
├── Buffer(数据容器)
│ ├── 四大属性:capacity/limit/position/mark
│ ├── 状态切换:flip/clear/compact/rewind
│ └── 类型:Heap/Direct/ReadOnly/Slice
├── Channel(传输通道)
│ ├── FileChannel(文件)
│ ├── SocketChannel(TCP客户端)
│ ├── ServerSocketChannel(TCP服务端)
│ ├── DatagramChannel(UDP)
│ └── AsynchronousChannel(NIO.2异步)
├── Selector(多路复用)
│ ├── 底层:select/poll/epoll/kqueue
│ ├── SelectionKey事件管理
│ └── wakeup/注册队列
├── 高级特性
│ ├── 零拷贝:transferTo/transferFrom/mmap
│ ├── Scatter/Gather
│ ├── FileLock文件锁
│ └── MappedByteBuffer内存映射
├── 设计模式
│ └── Reactor(单Reactor/主从Reactor)
└── 框架封装
└── Netty(EventLoop/Pipeline/ByteBuf)
18.2 面试高频问题
- NIO为什么比BIO快? ------ 非阻塞+多路复用减少线程数和上下文切换
- epoll比select好在哪? ------ O(1)事件通知、无fd数量限制、支持ET模式
- DirectBuffer什么时候用? ------ 大I/O、长生命周期、减少JNI拷贝
- 零拷贝原理? ------ 避免用户态和内核态之间的数据复制
- Reactor模式核心思想? ------ I/O事件驱动 + 事件分发 + 非阻塞处理
- Netty解决了NIO哪些问题? ------ 空轮询Bug、粘包拆包、内存池化、线程模型
- Selector线程安全吗? ------ 不是,注册/取消需要在Selector线程或通过wakeup协调
