NIO 性能调优

NIO 性能调优:零拷贝、直接内存与线上问题排查全解析 1 ⚡ 问题切入:文件服务器 CPU 100%,网络带宽却没用满 一个典型的文件下载服务,使用传统 Java I/O 实现: // 传统文件传输:将磁盘文件发送给客户端 public static void sendFile(Socket socket, String filepath) throws IOException { FileInputStream fis = new FileInputStream(filepath); BufferedInputStream bis = new BufferedInputStream(fis); OutputStream os = socket.getOutputStream(); byte[] buf = new byte[8192]; int len; while ((len = bis.read(buf)) != -1) { os.write(buf, 0, len); // 每次循环:内核→用户→内核→网卡 } bis.close(); fis.close(); } 这段代码能工作,但投入生产后出现异常现象:4 核 CPU 全部 100%,但千兆网卡只用了 600Mbps。理论上这台机器完全可以跑满千兆,为什么 CPU 先成了瓶颈? ...

九月 16, 2022 · 9 分钟 · 1755 字 · yaomingye

Netty

Netty:Reactor 线程模型、Pipeline 责任链与四个必写示例全解析 1 ⚡ 问题切入:原生 NIO 能工作,但你敢上生产吗? 在上一篇 Java NIO 博客中,我们手写了一个 NIO EchoServer——单线程管理多个连接,Selector 封装 epoll。这段代码在演示环境中运行良好,但如果直接部署到生产环境,会遇到四个棘手问题: // 原生 NIO EchoServer 的核心循环(看似正确,实则隐患重重) while (true) { selector.select(); for (SelectionKey key : selector.selectedKeys()) { if (key.isAcceptable()) { SocketChannel client = ssc.accept(); client.configureBlocking(false); client.register(selector, SelectionKey.OP_READ); } else if (key.isReadable()) { SocketChannel client = (SocketChannel) key.channel(); ByteBuffer buf = ByteBuffer.allocate(1024); int len = client.read(buf); // 问题1: 读到半包怎么办? buf.flip(); // 问题2: 忘了 flip 直接炸 client.write(buf); // 问题3: 写不出去谁管? buf.clear(); // 问题4: clear 还是 compact? } keyIterator.remove(); } } 原生 NIO 的四大生产痛点: ...

九月 15, 2022 · 13 分钟 · 2657 字 · yaomingye

Java NIO

Java NIO:epoll 多路复用、Buffer 机制与单线程高并发全解析 1 ⚡ 问题切入:一个线程如何管理 10000 个连接? 在经典的 BIO (Blocking I/O,阻塞 I/O)模型下,每个 Socket 连接需要分配一个独立线程。当 accept() 返回一个新连接,就启动一个线程去 read() —— 这个线程在数据到达之前会一直阻塞,CPU 时间被白白浪费在线程上下文切换上。 // 传统 BIO:一个连接一个线程(不可行) ServerSocket server = new ServerSocket(8080); while (true) { Socket client = server.accept(); // 阻塞等待连接 new Thread(() -> { InputStream in = client.getInputStream(); byte[] buf = new byte[1024]; in.read(buf); // 阻塞等待数据 // 处理数据... }).start(); } 问题:如果有 10,000 个连接,就需要 10,000 个线程。每个 Java 线程默认栈大小约 1MB,仅线程栈就消耗 10GB 内存,而且 CPU 绝大多数时间都在做线程切换而非真正处理数据。这就是著名的 C10K 问题 (Client 10,000 Problem)。 ...

九月 14, 2022 · 17 分钟 · 3496 字 · yaomingye

Linux IO 模型

Linux IO 模型:阻塞、非阻塞、多路复用与异步 IO 全解析 1 ⚡ 问题切入:一个后端开发者必须回答的问题 假设你在面试中被问到: “一台 4 核 8GB 的服务器,为什么能支撑 10 万个并发连接?” 答案的关键不在于 CPU 有多快、内存有多大,而在于 IO 模型 。如果每个连接用一个线程、每个线程做阻塞 IO,10 万连接就需要 10 万个线程——每个线程消耗约 1MB 栈空间,仅线程栈就占 100GB 内存,4 核 CPU 也根本无法调度这么多线程。 真正让高并发成为可能的,是 非阻塞 IO 和 IO 多路复用 (I/O Multiplexing,单个线程同时监听多个 IO 事件)。Nginx、Redis、Netty 的高性能都建立在正确的 IO 模型选择之上。 这篇博客从操作系统层面讲解 Linux 五大 IO 模型,聚焦于"数据如何从网卡/磁盘到达你的程序",为后续理解 Java NIO、Netty、Kafka 等框架打下理论基础。 2 💻 硬件架构:一次 IO 操作经历了什么 在讨论 IO 模型之前,必须先理解一次 IO 操作涉及哪些硬件组件以及数据如何流转。 如上图所示,一次典型的网络 IO 读取,数据经过以下路径: ...

九月 13, 2022 · 9 分钟 · 1736 字 · yaomingye

RandomAccessFile 文件指针与任意位置读写

RandomAccessFile 文件指针与任意位置读写:多线程分段下载实现 1 ⚡ 问题切入:流式读写的局限 前面介绍的 FileInputStream / FileOutputStream 都是 顺序流 (Sequential Stream),只能从文件头开始,一个字节接一个字节地读写。如果只想读文件的第 1000 个字节,顺序流必须先把前面 999 个字节读完(或调用 skip()),效率很低。 如果要实现以下场景,顺序流就完全不够用了: 修改一个 1GB 文件的中间某几个字节 在文件末尾追加日志,同时读取文件头部的元数据 多线程分段下载文件,每个线程负责不同区间的数据块 这些场景需要 随机读写 ——可以自由移动文件指针到任意位置,像操作数组下标一样操作文件。Java 提供了 RandomAccessFile 来实现这个能力。 // 随机读:直接跳到第 1000 字节开始读 RandomAccessFile raf = new RandomAccessFile("data.bin", "r"); raf.seek(1000); // 指针移到 position=1000 int b = raf.read(); // 读第 1000 个字节 System.out.printf("第1000字节: 0x%02X%n", b); raf.close(); 跳过前面 999 个字节,直达目标位置——这就是 RandomAccessFile 的价值。 2 🎯 核心概念:文件指针 2.1 ❓ 什么是文件指针 文件指针 (File Pointer)是 RandomAccessFile 内部维护的一个 long 类型偏移量(Offset),表示 下一次读写操作将发生的字节位置 。它类似于数组的下标索引,但作用对象是文件。 ...

九月 12, 2022 · 8 分钟 · 1611 字 · yaomingye

Java IO 进阶

Java IO 进阶:基本类型 IO、打印流与对象序列化实用指南 1 🎮 问题场景:从保存游戏分数说起 假设你正在开发一个本地小游戏,需要把玩家的最高分(int)、胜率(double)和昵称(String)保存到文件,下次启动时读回来。用前面学过的 FileWriter 写文本当然可以,但你需要手动处理类型转换——写的时候 int → String,读的时候 String → int,格式稍微不一致就解析失败。 有没有办法直接把 int 按固定 4 字节的二进制格式写入,读的时候也按 int 原样读出?这就是 基本类型 IO 要解决的问题。 更进一步,如果整个游戏状态是一个复杂的 Java 对象(玩家信息、关卡进度、道具列表),能不能 一键保存整个对象,一键读回 ?这就是 对象序列化 要解决的问题。 本篇覆盖 Java IO 的第四个进阶阶段,依次讲解三种机制: 机制 用途 一句话描述 DataInputStream / DataOutputStream 读写基本类型和字符串 按固定字节数的二进制格式读写,必须按序操作 PrintStream / PrintWriter 格式化文本输出 不抛异常,日常 System.out.println() 就在用它 ObjectInputStream / ObjectOutputStream 对象序列化与反序列化 一键将整个对象转为字节流保存或传输 2 💾 基本类型 IO:DataOutputStream / DataInputStream 2.1 ❓ 是什么 DataOutputStream 和 DataInputStream 是 装饰器流 (包装已有的 OutputStream / InputStream),提供直接读写 Java 基本类型和 String 的能力。数据以 平台无关的二进制格式 写入——比如 writeInt(42) 始终写 4 个字节,无论在什么操作系统上,读出来的值都不变。 ...

九月 9, 2022 · 5 分钟 · 937 字 · yaomingye

Java IO 编码与桥接

Java IO 编码与桥接:字符集、编码转换与乱码解决方案全解析 1 ⚠️ 问题切入:一段乱码代码 先看一段在实际开发中经常遇到的代码。这段代码在不同操作系统上运行,结果 完全不同 : public class GarbledDemo { public static void main(String[] args) throws Exception { // 在 Windows 中文系统上运行(默认 GBK) try (FileWriter writer = new FileWriter("hello.txt")) { writer.write("你好,世界!"); } // 在 Linux 服务器上读取(默认 UTF-8) try (FileReader reader = new FileReader("hello.txt")) { char[] buf = new char[1024]; int len = reader.read(buf); System.out.println(new String(buf, 0, len)); // 输出:你好,世界! ← 正常 // 还是:���← 乱码? // 取决于操作系统! } } } 为什么同一段代码在不同环境下表现不同?因为 FileReader / FileWriter 使用 JVM 默认编码 (通常是操作系统默认编码),而 Windows 中文版默认是 GBK ,Linux 默认是 UTF-8 。写入和读取时编码不一致,就会产生 乱码 (Mojibake,指因字符编码不匹配导致的不可读字符)。 ...

九月 9, 2022 · 8 分钟 · 1535 字 · yaomingye

Java IO 缓冲流与装饰器模式

Java IO 缓冲流与装饰器模式:从性能对比到设计模式全解析 1 ⚡ 问题切入:为什么单字节读取一个 10MB 文件要 30 秒? 先看一段能跑的代码。下面这段程序用 FileInputStream 的 read() 方法,一个字节一个字节地读取一个 10MB 的文件,并写入到另一个文件: // 无缓冲:单字节读取 try (FileInputStream fis = new FileInputStream("source_10mb.bin"); FileOutputStream fos = new FileOutputStream("dest_10mb.bin")) { int b; while ((b = fis.read()) != -1) { // 每次只读 1 字节 fos.write(b); // 每次只写 1 字节 } } 在我的机器上(Windows 11,SSD),这段代码的运行耗时约为 28,000 ms (28 秒)。 现在再看另一段代码,功能完全一样,只是在外层各包了一个缓冲流: // 有缓冲:仍然单字节读取 try (BufferedInputStream bis = new BufferedInputStream( new FileInputStream("source_10mb.bin")); BufferedOutputStream bos = new BufferedOutputStream( new FileOutputStream("dest_10mb.bin"))) { int b; while ((b = bis.read()) != -1) { // 仍然每次只读 1 字节 bos.write(b); // 仍然每次只写 1 字节 } } 耗时:约 150 ms 。两者相差约 180 倍 。代码逻辑完全一样(都是循环内单字节读写),一行包的差别,性能天差地别。 ...

九月 9, 2022 · 10 分钟 · 1942 字 · yaomingye

Java IO API 基础入门

Java IO API 基础入门:File 类、字节流与字符流使用指南 1 📁 File 类(文件路径操作) java.io.File 是 Java IO 包中最基础的类,它表示文件系统中的一个 路径 (文件或目录),提供创建、删除、判断、遍历等操作。核心概念:File 对象只是一个 路径的抽象表示 ,创建 File 对象时不会检查文件或目录在磁盘上是否真实存在。 1.1 🏗️ 构造器与路径表示 构造器 说明 new File("path") 接收一个路径字符串,支持相对路径和绝对路径 new File("parent", "child") 接收父路径和子路径,自动拼接分隔符 File f1 = new File("D:/data/test.txt"); // 绝对路径 File f2 = new File("./data/test.txt"); // 相对路径(相对于项目根目录) File f3 = new File("D:/data", "test.txt"); // 父路径 + 子路径 关键陷阱 :new File("path") 只是在内存中构造了一个路径对象,不会检查文件是否真实存在 ,也不会创建文件。这意味着即使路径指向一个不存在的文件,构造器也不会抛异常。 1.2 🔍 文件/目录判断 判断一个路径在磁盘上是否存在、是文件还是目录: 方法 返回值 说明 exists() boolean 路径对应的文件或目录是否存在 isFile() boolean 是否存在且是文件(不是目录) isDirectory() boolean 是否存在且是目录(不是文件) File file = new File("D:/data/test.txt"); if (file.exists()) { System.out.println(file.isFile() ? "是文件" : "是目录"); } else { System.out.println("路径不存在"); } 注意事项: ...

九月 7, 2022 · 5 分钟 · 1024 字 · yaomingye
Cat Radio