Java AIO vs NIO
public class NioFileDemo {
public static void main(String[] args) {
NioFileDemo demo = new NioFileDemo();
demo.writeFile();
demo.readFile();
}
// 使用 NIO 写入文件
public void writeFile() {
Path path = Paths.get("logs/itwanger/paicoding.txt");
try {
FileChannel fileChannel = FileChannel.open(path, EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE));
ByteBuffer buffer = StandardCharsets.UTF_8.encode("学编程就上技术派");
fileChannel.write(buffer);
System.out.println("写入完成");
fileChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 使用 NIO 读取文件
public void readFile() {
Path path = Paths.get("logs/itwanger/paicoding.txt");
try {
FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = fileChannel.read(buffer);
while (bytesRead != -1) {
buffer.flip();
System.out.println("读取的内容: " + StandardCharsets.UTF_8.decode(buffer));
buffer.clear();
bytesRead = fileChannel.read(buffer);
}
fileChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}public class AioDemo {
public static void main(String[] args) {
AioDemo demo = new AioDemo();
demo.writeFile();
demo.readFile();
}
// 使用 AsynchronousFileChannel 写入文件
public void writeFile() {
// 使用 Paths.get() 获取文件路径
Path path = Paths.get("logs/itwanger/paicoding.txt");
try {
// 用 AsynchronousFileChannel.open() 打开文件通道,指定写入和创建文件的选项。
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
// 将要写入的字符串("学编程就上技术派")转换为 ByteBuffer。
ByteBuffer buffer = StandardCharsets.UTF_8.encode("学编程就上技术派");
// 调用 fileChannel.write() 方法将 ByteBuffer 中的内容写入文件。这是一个异步操作,因此需要使用 Future 对象等待写入操作完成。
Future<Integer> result = fileChannel.write(buffer, 0);
// 等待写操作完成
result.get();
System.out.println("写入完成");
fileChannel.close();
} catch (IOException | InterruptedException | java.util.concurrent.ExecutionException e) {
e.printStackTrace();
}
}
// 使用 AsynchronousFileChannel 读取文件
public void readFile() {
Path path = Paths.get("logs/itwanger/paicoding.txt");
try {
// 指定读取文件的选项。
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
// 创建一个 ByteBuffer,用于存储从文件中读取的数据。
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 调用 fileChannel.read() 方法从文件中异步读取数据。该方法接受一个 CompletionHandler 对象,用于处理异步操作完成后的回调。
fileChannel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
// 在 CompletionHandler 的 completed() 方法中,翻转 ByteBuffer(attachment.flip()),然后使用 Charset.forName("UTF-8").decode() 将其解码为字符串并打印。最后,清空缓冲区并关闭文件通道。
attachment.flip();
System.out.println("读取的内容: " + StandardCharsets.UTF_8.decode(attachment));
attachment.clear();
try {
fileChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
// 如果异步读取操作失败,CompletionHandler 的 failed() 方法将被调用,打印错误信息。
System.out.println("读取失败");
exc.printStackTrace();
}
});
// 等待异步操作完成
Thread.sleep(1000);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}为什么使用 AIO 的代码看起来更复杂了?
非常好!你观察得很敏锐。虽然 AIO(Asynchronous I/O,异步 I/O) 理论上是比 NIO(Non-blocking I/O,非阻塞 I/O) 更先进的 I/O 模型,但在代码层面上,AIO 代码确实显得更“复杂”。我们来仔细分析为什么会这样。
💡 一、NIO vs AIO 的编程模型不同
| 特点 | NIO | AIO |
|---|---|---|
| 模型 | 同步非阻塞(由程序主动“轮询”数据是否可读/可写) | 异步非阻塞(由系统回调通知程序操作完成) |
| 控制权 | 主程序自己控制读写时机 | 控制权交给操作系统/回调机制 |
| 操作风格 | 顺序执行(像普通文件读写) | 回调式(要注册 CompletionHandler) |
| 理解成本 | 简单直观 | 逻辑分散、回调代码多、调试不便 |
NIO 的设计理念是“我询问系统:现在能不能读/写?”
AIO 则是“我告诉系统:帮我读/写,完成了你再通知我。”
🧩 二、代码复杂度的来源
1️⃣ 异步编程带来了回调逻辑
在 AIO 中,为了异步执行并在完成时通知,你必须提供一个 CompletionHandler 或 Future 来接收结果。
这就导致:
- 代码结构从顺序执行 → 回调嵌套式执行;
- 错误处理逻辑需要分散编写;
- 需要显式等待或协调线程(例如
Thread.sleep())。
举个形象的例子:
- NIO 就像是你一直在问:“文件写完了吗?好,我继续干别的。”
- AIO 则是你说:“写文件这件事交给你了,写完打我电话通知我。”
但这意味着你要提供“打电话之后要干什么”的完整逻辑(CompletionHandler)。
2️⃣ 需要同时考虑 Future 和 CompletionHandler 两种写法
AIO 支持两种调用风格:
Future模式(阻塞直到异步操作完成);CompletionHandler模式(回调通知结果)。
这虽然提供了灵活性,但也导致代码显得更啰嗦,因为你往往要写更多的类型声明和异常处理逻辑。
3️⃣ 为了健壮性,AIO 必须显式控制线程或延迟
异步回调是系统在后台线程中执行的,主线程可能在回调发生前就退出。因此你在示例代码中看到需要:
Thread.sleep(1000);这一行是为了防止主线程结束,把回调线程一同“带死”;这也是初学者常觉得“麻烦”的地方。
⚙️ 三、总结:AIO 更复杂但更适用大并发场景
| 对比维度 | NIO | AIO |
|---|---|---|
| 任务模型 | 同步轮询 | 异步回调 |
| 代码复杂度 | 简洁 | 较复杂 |
| CPU 开销 | 较高(轮询) | 较低(事件通知) |
| 适用场景 | 少量 I/O、顺序执行任务 | 高并发、大量 I/O、服务端网络通信 |
| 学习曲线 | 平缓 | 陡峭 |
🔍 四、一句话总结
AIO 更智能,但需要你写更多“协议”告诉系统该怎么通知你;NIO 更原始,但控制更直观。
换句话说,
AIO 复杂的是“结构”,简单的是“运行”;
NIO 简单的是“结构”,复杂的是“调度”。
确实很多人都说Java的代码看起来更啰嗦,那么如果是Go 语言实现同样的功能,代码会更简洁吗?
非常好的问题 👍
你提到了一个关键感受 —— Java 代码“啰嗦”,而 Go 语言(Golang) 常被称为“简洁、直观、高效”。我们确实可以从你给出的这个 I/O 场景中清楚地看出区别。
🧩 一、Java VS Go 的设计哲学差异
| 维度 | Java | Go |
|---|---|---|
| 编程范式 | 面向对象(OOP),强调类型体系和结构化 | 面向过程 + 并发优先,追求简洁 |
| 异步模型 | 基于回调(AIO)、Future、CompletableFuture、Reactive 等 | 基于 goroutine,语法层级支持并发 |
| 错误处理 | try...catch 异常系统 | 明确返回 error,结构更扁平 |
| I/O 模型 | 传统上依赖 NIO/AIO 通道模型 | 内置 goroutine 与通道(channel),并发读写更自然 |
| 语言冗余度 | 大量接口、泛型、类型声明 | 小而美、编译快、自带工具链 |
简而言之:
Go 通过语言级并发模型,就天然带有“异步 I/O 的味道”,而不需要像 Java 一样额外编写回调逻辑。
💡 二、同样功能的示例:Go 版文件读写
下面的 Go 代码实现了写入文件 + 读取文件的功能,与 Java 的 NIO / AIO 示例完全等价。
package main
import (
"fmt"
"io/ioutil"
"os"
)
func main() {
writeFile()
readFile()
}
// 写文件
func writeFile() {
path := "logs/itwanger/paicoding.txt"
// 确保目录存在
os.MkdirAll("logs/itwanger", os.ModePerm)
content := []byte("学编程就上技术派")
err := ioutil.WriteFile(path, content, 0644)
if err != nil {
fmt.Println("写入失败:", err)
return
}
fmt.Println("写入完成")
}
// 读文件
func readFile() {
path := "logs/itwanger/paicoding.txt"
content, err := ioutil.ReadFile(path)
if err != nil {
fmt.Println("读取失败:", err)
return
}
fmt.Println("读取的内容:", string(content))
}✅ 输出示例:
写入完成
读取的内容: 学编程就上技术派🧠 三、比较下差异
| 功能点 | Java (NIO/AIO) | Go |
|---|---|---|
| 写文件 | FileChannel.open + ByteBuffer + StandardOpenOption | ioutil.WriteFile() |
| 读文件 | FileChannel.open + ByteBuffer + decode() | ioutil.ReadFile() |
| 异步 I/O | 需要 Future、CompletionHandler | 天生支持 goroutine |
| 错误处理 | try-catch 块中抛异常 | if err != nil {} 明确返回 |
| 代码行数 | 50+ | 15 左右 |
| 心智负担 | 较重(接口 & Channel 概念) | 较轻(直接明确的 I/O 操作) |
⚙️ 四、如果想用异步方式(例如模拟 Java AIO)
在 Go 中你甚至只需要一行 go 关键字:
go writeFile() // 异步写
go readFile() // 异步读因为 每个 goroutine 本质上就是一个轻量协程,你不需要包装 Future、注册 CompletionHandler 或使用线程池,Go 运行时自动帮你管理调度。
🚀 五、简洁源于语言内核
| 语言特性 | Go 的内置支持 |
|---|---|
| 并发 | ✅ goroutine、channel |
| 错误机制 | ✅ 明确 error 返回 |
| 文件读写 | ✅ ioutil / os 包统一接口 |
| 工具链 | ✅ go fmt、go build、go run 一体化 |
| 语法冗余 | ❌ 无泛型装饰、无继承树 |
🏁 六、总结一句话
✅ Java 是“工程化的语言”,给你所有控制权;
✅ Go 是“工程师的语言”,给你最直接的路径。
GO 的代码确实更简洁,但是如果是JDK 25,支持协程的Java呢?
非常棒的问题!👏
你提到 JDK 25 支持协程(虚拟线程,Virtual Threads) —— 这个是 Java 近几年最重要、最革命性的变化之一,也正是让 Java 向 Go 靠拢的关键一步。
我们来深入聊一下这个问题,看看在 “支持协程的 Java” 时代,Java 代码是否能变得和 Go 一样简洁、优雅。
☕ 一、JDK 25:协程 = 虚拟线程(Virtual Threads)
协程(Coroutine)是一种用户级线程技术,
而 Java 的虚拟线程(Virtual Thread) 就是对协程理念的官方落地。
✅ 基本原理
在传统 Java 中:
- 每个线程都对应一个 操作系统级线程(OS thread)。
- 操作系统调度线程 → 成本高昂(尤其在百万并发时)。
在 JDK 21(正式引入)→ JDK 25(稳定优化) 之后:
- Java 引入 Virtual Threads,由 JVM 调度,而非 OS。
- 它们是 轻量级的线程,挂起和恢复都由 JVM 管理。
- 语义仍然是“阻塞式”的,看起来是同步写法,但运行时是异步的。
💡一句话理解:
Java 25 的虚拟线程 = Go 的 goroutine。
🧩 二、你原来的 AIO 示例,用虚拟线程改写后是什么样?
我们先看看你之前的 AIO 示例里最“繁琐”的部分:
AsynchronousFileChannelCompletionHandlerFuture.get()Thread.sleep()等待异步完成
在 Java 25 中,我们可以用 普通 I/O(同步写法)+ 虚拟线程,完全消除这些复杂性。
✅ 改写示例(JDK 25 / Project Loom 风格)
import java.io.IOException;
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
public class VirtualThreadIODemo {
public static void main(String[] args) throws Exception {
// 启动两个虚拟线程同时读写
Thread writer = Thread.startVirtualThread(() -> {
try {
writeFile();
} catch (IOException e) {
e.printStackTrace();
}
});
Thread reader = Thread.startVirtualThread(() -> {
try {
// 暂停一下,确保文件写好
Thread.sleep(100);
readFile();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
});
writer.join();
reader.join();
}
static void writeFile() throws IOException {
Path path = Paths.get("logs/itwanger/paicoding.txt");
Files.createDirectories(path.getParent());
Files.writeString(path, "学编程就上技术派", StandardCharsets.UTF_8);
System.out.println("写入完成");
}
static void readFile() throws IOException {
Path path = Paths.get("logs/itwanger/paicoding.txt");
String content = Files.readString(path, StandardCharsets.UTF_8);
System.out.println("读取的内容: " + content);
}
}输出示例:
写入完成
读取的内容: 学编程就上技术派⚡ 三、对比结果:和 Go 的差距几乎消失了
| 特性 | 传统 Java (AIO) | 新式 Java (虚拟线程) | Go |
|---|---|---|---|
| 并发模型 | Future / CompletionHandler | 虚拟线程(协程调度) | Goroutine |
| 写法风格 | 回调 / 异步繁琐 | 同步写法、异步执行 | 同步写法、异步执行 |
| 性能 | 操作系统线程调度 | JVM 用户态调度 | Go runtime 调度 |
| 创建开销 | 高(MB级栈空间) | 极低(KB级) | 极低(KB级) |
| 代码简洁度 | ❌ 啰嗦 | ✅ 接近 Go | ✅ 简洁 |
| 工程生态 | ✅ 成熟 | ✅ 向 Go 看齐 | ✅ 简洁灵活 |
🧠 四、为什么协程让 Java 变简洁
- 去掉了回调地狱
无需再写Future、CompletionHandler、CompletableFuture。
所有异步任务都可以“看起来同步,实际上异步”。 - 不再需要线程池管理
虚拟线程由 JVM 调度和复用,不会像以往那样创建几千个就 OOM。 - 兼容原生 API
不需要修改原有的Files.*、Socket.*等阻塞式 API,直接可用。 - 极为高效的并发
实测百万虚拟线程只占几百 MB 内存,几乎和 Go 的 goroutine 数量级相同。
🌈 五、一句话总结
☕ JDK 25 的虚拟线程让 Java 拥有了 Go 式的并发能力与简洁语义。
如今的 Java 终于可以做到:
- 写起来是同步风格;
- 跑起来是异步高并发;
- 生态上仍然是 "老 Java" 的稳健性。
评论已关闭