IO 高频面试题
面试问到 IO,十有八九会扯到 BIO、NIO、零拷贝、序列化。本节用矩阵对比的方式,把这些问题讲清楚。
BIO vs NIO vs AIO
| 特性 | BIO | NIO | AIO |
|---|---|---|---|
| 全称 | Blocking IO | New IO / Non-blocking IO | Asynchronous IO |
| IO 类型 | 同步阻塞 | 同步非阻塞 | 异步非阻塞 |
| 线程模型 | 1 连接 1 线程 | 1 线程管 N 连接 | 事件驱动 |
| 适用场景 | 连接少(< 1000) | 高并发(> 1000) | IO 密集 |
| 复杂度 | 低 | 中 | 高 |
| JDK 版本 | 1.0 | 1.4 | 1.7 |
BIO:每个连接一个线程
java
// 一个线程处理一个连接
ServerSocket server = new ServerSocket(8080);
while (true) {
Socket client = server.accept(); // 阻塞
new Thread(() -> handle(client)).start(); // 为每个连接创建线程
}问题:10000 连接 = 10000 线程 = 内存爆炸,上下文切换开销巨大。
NIO:多路复用,1 线程管 N 连接
java
// 一个线程管理多个连接
ServerSocketChannel server = ServerSocketChannel.open();
server.socket().bind(new InetSocketAddress(8080));
server.configureBlocking(false); // 非阻塞
Selector selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);
while (selector.select() > 0) {
for (SelectionKey key : selector.selectedKeys()) {
if (key.isAcceptable()) {
// 处理新连接
} else if (key.isReadable()) {
// 处理读事件
} else if (key.isWritable()) {
// 处理写事件
}
}
selector.selectedKeys().clear();
}优势:单线程管理大量连接,适合高并发场景。
AIO:异步回调
java
// 操作发起后立即返回,IO 完成时回调
AsynchronousServerSocketChannel server =
AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(8080));
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel client, Void attachment) {
server.accept(null, this); // 继续接受下一个
ByteBuffer buf = ByteBuffer.allocate(1024);
client.read(buf, null, new CompletionHandler<Integer, Void>() {
@Override
public void completed(Integer result, Void attachment) {
buf.flip();
// 处理读取的数据
}
});
}
});NIO 三大核心组件
| 组件 | 作用 | 比喻 |
|---|---|---|
| Buffer | 缓冲区,存储数据 | 水缸 |
| Channel | 通道,传输数据 | 水管 |
| Selector | 选择器,监听事件 | 阀门 |
Buffer:数据的载体
java
ByteBuffer buf = ByteBuffer.allocate(1024);
// 写模式
buf.put("hello".getBytes());
// 切换读模式
buf.flip();
// 读数据
while (buf.hasRemaining()) {
System.out.print((char) buf.get());
}
// 清空,准备下次写入
buf.clear();Channel:数据的通道
java
FileChannel in = new FileInputStream("file.txt").getChannel();
FileChannel out = new FileOutputStream("out.txt").getChannel();
ByteBuffer buf = ByteBuffer.allocate(8192);
while (in.read(buf) != -1) {
buf.flip();
out.write(buf);
buf.clear();
}零拷贝
传统文件传输需要 4 次拷贝:
磁盘 → 内核缓冲区 → 用户缓冲区 → Socket 缓冲区 → 网卡零拷贝只需要 2 次拷贝:
磁盘 → 内核缓冲区(sendfile)→ 网卡Java 实现:
java
FileChannel in = new FileInputStream("bigfile.mp4").getChannel();
FileChannel out = new FileOutputStream("copy.mp4").getChannel();
long size = in.size();
long transferred = 0;
while (transferred < size) {
transferred += in.transferTo(transferred, size - transferred, out);
}适用场景:大文件传输,如视频服务、文件服务器。
序列化与反序列化
| 问题 | 答案 |
|---|---|
| 什么可以序列化? | 实现 Serializable 接口的对象 |
| serialVersionUID 是什么? | 版本号,反序列化时对比,版本不一致抛异常 |
| transient 关键字的作用? | 跳过字段,不参与序列化 |
java
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private transient String password; // 不序列化
private int age;
}
// 自定义序列化
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeObject(encrypt(password));
}
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
this.password = decrypt((String) in.readObject());
}Java 序列化 vs JSON
| 特性 | Java 序列化 | JSON |
|---|---|---|
| 格式 | 二进制 | 文本 |
| 体积 | 小 | 较大 |
| 跨语言 | ❌ | ✅ |
| 性能 | 快 | 较慢 |
字符编码
为什么会乱码? 编码和解码用了不同的字符集。
"中" → UTF-8 → [0xE4, 0xBD, 0xA0] → 用 GBK 解码 → 乱码!如何避免?
java
// ❌ 错误:依赖平台默认编码
new FileReader("file.txt");
// ✅ 正确:显式指定 UTF-8
new InputStreamReader(
new FileInputStream("file.txt"),
StandardCharsets.UTF_8);
// ✅ JDK 11+
String content = Files.readString(Path.of("file.txt"), StandardCharsets.UTF_8);资源泄漏
java
// ❌ 没有关闭,抛异常时泄漏
FileInputStream fis = new FileInputStream("file.txt");
// ✅ try-with-resources
try (FileInputStream fis = new FileInputStream("file.txt")) {
// 自动关闭
}记住这个面试重点矩阵:
| 问题 | 核心答案 |
|---|---|
| BIO vs NIO vs AIO | 阻塞 vs 非阻塞 vs 异步 |
| NIO 三件套 | Buffer、Channel、Selector |
| 零拷贝 | transferTo() 减少拷贝 |
| 序列化 | serialVersionUID 控制版本,transient 跳过字段 |
| 字符编码 | 显式指定 UTF-8 |
| 资源泄漏 | try-with-resources |
面试不是背书,理解原理才能举一反三。
