diff --git a/src/main/java/org/example/java_base_test/TreeSetCompare.java b/src/main/java/org/example/java_base_test/TreeSetCompare.java new file mode 100644 index 0000000..357cce3 --- /dev/null +++ b/src/main/java/org/example/java_base_test/TreeSetCompare.java @@ -0,0 +1,467 @@ +package org.example.java_base_test; + +import java.util.Comparator; +import java.util.TreeSet; + +/** + * @Description TreeSetCompare - TreeSet排序原理详解 + * @Author 98468 + * @Date 2026-07-09 + * @Remark 通过代码示例讲解TreeSet的Comparator工作原理 + * @Copyright Copyright © XX公司 All Rights Reserved + */ +public class TreeSetCompare { + + public static void main(String[] args) { + System.out.println("========== TreeSet 排序原理详解 =========="); + System.out.println(); + + // 1. 核心法则演示 + demoCoreRule(); + + // 2. 升序实现 + demoAscendingOrder(); + + // 3. 降序实现 + demoDescendingOrder(); + + // 4. 多级排序 + demoMultiLevelSort(); + + // 5. 不同类型比较 + demoDifferentTypes(); + + // 6. 去重机制 + demoDeduplication(); + + // 7. 逆序排序与红黑树遍历详解 + demoReverseOrderWithTreeTraversal(); + } + + /** + * 1. 核心法则:返回值决定顺序(铁律) + * + * TreeSet(红黑树)只认compare返回值的符号,不认逻辑本身: + * - 负数 (-1): o1 小于 o2 → o1 排在 o2 前面(左子树) + * - 零 (0): o1 等于 o2 → 去重(后添加的忽略) + * - 正数 (1): o1 大于 o2 → o1 排在 o2 后面(右子树) + */ + private static void demoCoreRule() { + System.out.println("【1】核心法则:返回值决定顺序"); + System.out.println("-----------------------------------"); + System.out.println("TreeSet只认compare返回值的符号:"); + System.out.println(" 负数(-1) → o1 < o2 → o1排前面"); + System.out.println(" 零(0) → o1 = o2 → 去重"); + System.out.println(" 正数(1) → o1 > o2 → o1排后面"); + System.out.println(); + + // 创建一个简单的比较器,手动控制返回值 + Comparator manualComparator = (o1, o2) -> { + if (o1 < o2) return -1; // 负数:o1排前面 + if (o1.equals(o2)) return 0; // 零:去重 + return 1; // 正数:o1排后面 + }; + + TreeSet treeSet = new TreeSet<>(manualComparator); + treeSet.add(30); + treeSet.add(10); + treeSet.add(20); + treeSet.add(10); // 重复,会被去重 + + System.out.println("添加顺序: 30, 10, 20, 10(重复)"); + System.out.println("TreeSet结果: " + treeSet); + System.out.println("说明: 自动按升序排列,且去重"); + System.out.println(); + } + + /** + * 2. 升序实现原理(标准写法) + * + * Integer.compare(o1.getAge(), o2.getAge()) + * 底层逻辑:compare(x, y) 返回 x - y 的符号 + * 参数顺序:前者减后者(o1 - o2) + * 结果:年龄小的o1返回负数 → 被树判定为"小于" → 自然排在前面 → 年龄升序 + */ + private static void demoAscendingOrder() { + System.out.println("【2】升序实现(标准写法)"); + System.out.println("-----------------------------------"); + + // 使用Integer.compare实现升序 + Comparator ageAscComparator = (o1, o2) -> + Integer.compare(o1.getAge(), o2.getAge()); + + TreeSet ascSet = new TreeSet<>(ageAscComparator); + ascSet.add(new Person("张三", 28)); + ascSet.add(new Person("李四", 18)); + ascSet.add(new Person("王五", 25)); + + System.out.println("比较器: Integer.compare(o1.getAge(), o2.getAge())"); + System.out.println("逻辑: o1.age - o2.age(前者减后者)"); + System.out.println("添加顺序: 张三(28), 李四(18), 王五(25)"); + System.out.println("TreeSet结果(按年龄升序):"); + for (Person p : ascSet) { + System.out.println(" " + p); + } + System.out.println(); + } + + /** + * 3. 降序实现原理(调换参数) + * + * Integer.compare(o2.getAge(), o1.getAge()) + * 底层逻辑:变成 o2 - o1 + * 结果:当o1=18,o2=28时,实际比较28-18得正数 → 树将o1判定为"大于"o2 → 18被排到28后面 → 年龄降序 + */ + private static void demoDescendingOrder() { + System.out.println("【3】降序实现(调换参数)"); + System.out.println("-----------------------------------"); + + // 使用Integer.compare实现降序(参数反写) + Comparator ageDescComparator = (o1, o2) -> + Integer.compare(o2.getAge(), o1.getAge()); + + TreeSet descSet = new TreeSet<>(ageDescComparator); + descSet.add(new Person("张三", 28)); + descSet.add(new Person("李四", 18)); + descSet.add(new Person("王五", 25)); + + System.out.println("比较器: Integer.compare(o2.getAge(), o1.getAge())"); + System.out.println("逻辑: o2.age - o1.age(参数反写)"); + System.out.println("添加顺序: 张三(28), 李四(18), 王五(25)"); + System.out.println("TreeSet结果(按年龄降序):"); + for (Person p : descSet) { + System.out.println(" " + p); + } + System.out.println(); + } + + /** + * 4. 多级排序(级联比较)标准模板 + * + * 当第一条件相等时,必须逐级向后传递,只有当前一级res==0时才比较下一级: + * int res = 第一条件; + * if (res == 0) res = 第二条件; + * if (res == 0) res = 第三条件; + * return res; + * + * 作用:确保排序唯一性,且若所有指定字段相等则返回0,自动去重 + */ + private static void demoMultiLevelSort() { + System.out.println("【4】多级排序(级联比较)"); + System.out.println("-----------------------------------"); + + // 多级排序:先按年龄升序,年龄相同再按姓名字典序升序 + Comparator multiLevelComparator = (o1, o2) -> { + // 第一级:按年龄升序 + int res = Integer.compare(o1.getAge(), o2.getAge()); + + // 如果年龄相同,进入第二级:按姓名升序 + if (res == 0) { + res = o1.getName().compareTo(o2.getName()); + } + + return res; + }; + + TreeSet multiSet = new TreeSet<>(multiLevelComparator); + multiSet.add(new Person("张三", 25)); + multiSet.add(new Person("李四", 18)); + multiSet.add(new Person("王五", 25)); // 年龄与张三相同 + multiSet.add(new Person("赵六", 18)); // 年龄与李四相同 + multiSet.add(new Person("张三", 25)); // 完全重复,会被去重 + + System.out.println("比较器: 先按年龄升序,年龄相同按姓名升序"); + System.out.println("添加顺序: 张三(25), 李四(18), 王五(25), 赵六(18), 张三(25)(重复)"); + System.out.println("TreeSet结果(多级排序):"); + for (Person p : multiSet) { + System.out.println(" " + p); + } + System.out.println("说明: 18岁的赵六和李四按姓名排序,25岁的张三和王五按姓名排序"); + System.out.println(); + } + + /** + * 5. 各类型比较的正确写法 + * + * 避免精度损失或空指针,请使用包装类提供的静态方法: + * - 整数(int):Integer.compare(o1.getAge(), o2.getAge()) + * - 浮点数(double):Double.compare(o1.getHeight(), o2.getHeight()) + * - 字符串(String):o1.getName().compareTo(o2.getName())(已按字典序) + */ + private static void demoDifferentTypes() { + System.out.println("【5】不同类型比较的正确写法"); + System.out.println("-----------------------------------"); + + // 创建带身高信息的扩展Person类 + class PersonWithHeight extends Person { + private double height; + + public PersonWithHeight(String name, int age, double height) { + super(name, age); + this.height = height; + } + + public double getHeight() { + return height; + } + + @Override + public String toString() { + return "Person{" + + "name='" + getName() + '\'' + + ", age=" + getAge() + + ", height=" + height + + '}'; + } + } + + // 多类型比较器:年龄(int) + 身高(double) + 姓名(String) + Comparator multiTypeComparator = (o1, o2) -> { + // 第一级:按年龄升序(使用Integer.compare) + int res = Integer.compare(o1.getAge(), o2.getAge()); + + // 第二级:按身高升序(使用Double.compare处理精度) + if (res == 0) { + res = Double.compare(o1.getHeight(), o2.getHeight()); + } + + // 第三级:按姓名字典序(使用String.compareTo) + if (res == 0) { + res = o1.getName().compareTo(o2.getName()); + } + + return res; + }; + + TreeSet typeSet = new TreeSet<>(multiTypeComparator); + typeSet.add(new PersonWithHeight("张三", 25, 1.75)); + typeSet.add(new PersonWithHeight("李四", 18, 1.80)); + typeSet.add(new PersonWithHeight("王五", 25, 1.70)); + typeSet.add(new PersonWithHeight("赵六", 25, 1.75)); // 年龄身高与张三相同 + + System.out.println("比较器: 年龄(Integer.compare) → 身高(Double.compare) → 姓名(String.compareTo)"); + System.out.println("添加顺序:"); + System.out.println(" 张三(25岁, 1.75m), 李四(18岁, 1.80m)"); + System.out.println(" 王五(25岁, 1.70m), 赵六(25岁, 1.75m)"); + System.out.println("TreeSet结果(多类型排序):"); + for (PersonWithHeight p : typeSet) { + System.out.println(" " + p); + } + System.out.println("说明: "); + System.out.println(" - 18岁李四排最前"); + System.out.println(" - 25岁三人中,王五(1.70m) < 张三/赵六(1.75m)"); + System.out.println(" - 张三和赵六身高相同,按姓名字典序:张三 < 赵六"); + System.out.println(); + } + + /** + * 6. 去重机制演示 + * + * 去重依据:只有最终res==0才算重复 + * 若仅年龄相等但姓名不同,不会去重 + */ + private static void demoDeduplication() { + System.out.println("【6】去重机制"); + System.out.println("-----------------------------------"); + + // 只按年龄比较的比较器 + Comparator ageOnlyComparator = (o1, o2) -> + Integer.compare(o1.getAge(), o2.getAge()); + + TreeSet dedupSet = new TreeSet<>(ageOnlyComparator); + dedupSet.add(new Person("张三", 25)); + dedupSet.add(new Person("李四", 25)); // 年龄相同,姓名不同 + dedupSet.add(new Person("王五", 25)); // 年龄相同,姓名不同 + dedupSet.add(new Person("张三", 25)); // 完全相同的年龄 + + System.out.println("比较器: 只按年龄比较"); + System.out.println("添加顺序: 张三(25), 李四(25), 王五(25), 张三(25)"); + System.out.println("TreeSet结果:"); + for (Person p : dedupSet) { + System.out.println(" " + p); + } + System.out.println("说明: "); + System.out.println(" - 因为比较器只比较年龄,所以年龄相同的都被视为'相等'"); + System.out.println(" - 只会保留第一个添加的张三(25),后面的都被去重了"); + System.out.println(); + + // 对比:使用多级排序的比较器 + Comparator multiComparator = (o1, o2) -> { + int res = Integer.compare(o1.getAge(), o2.getAge()); + if (res == 0) { + res = o1.getName().compareTo(o2.getName()); + } + return res; + }; + + TreeSet multiDedupSet = new TreeSet<>(multiComparator); + multiDedupSet.add(new Person("张三", 25)); + multiDedupSet.add(new Person("李四", 25)); // 年龄相同,姓名不同 + multiDedupSet.add(new Person("王五", 25)); // 年龄相同,姓名不同 + multiDedupSet.add(new Person("张三", 25)); // 完全重复 + + System.out.println("比较器: 先按年龄,再按姓名"); + System.out.println("添加顺序: 张三(25), 李四(25), 王五(25), 张三(25)"); + System.out.println("TreeSet结果:"); + for (Person p : multiDedupSet) { + System.out.println(" " + p); + } + System.out.println("说明: "); + System.out.println(" - 年龄相同但姓名不同,不会被去重"); + System.out.println(" - 只有完全相同的张三(25)才会被去重"); + System.out.println(); + } + + /** + * 7. 逆序排序与红黑树遍历详解 + * + * 这个例子会详细展示: + * 1. 逆序比较器的实现原理 + * 2. 元素逐个添加到红黑树的过程 + * 3. 中序遍历如何得到逆序结果 + */ + private static void demoReverseOrderWithTreeTraversal() { + System.out.println("【7】逆序排序与红黑树遍历详解"); + System.out.println("========================================="); + System.out.println(); + + System.out.println("📌 核心概念回顾:"); + System.out.println(" • TreeSet底层是红黑树(自平衡二叉搜索树)"); + System.out.println(" • 左子树 < 根节点 < 右子树"); + System.out.println(" • 中序遍历(左→根→右)得到有序序列"); + System.out.println(" • 逆序 = 改变比较规则,让大的数排在左边"); + System.out.println(); + + // 创建逆序比较器 + Comparator reverseComparator = (o1, o2) -> { + // 关键:参数反写,o2 - o1 + // 当o1=30, o2=10时,返回 10-30 = -20(负数) + // 树认为 o1 < o2,所以30会被放到10的左边 + return Integer.compare(o2, o1); + }; + + TreeSet reverseSet = new TreeSet<>(reverseComparator); + + System.out.println("🔍 逐步添加元素到红黑树的过程:"); + System.out.println("比较器逻辑: Integer.compare(o2, o1) → 大的数放左边"); + System.out.println(); + + // 第1步:添加 30 + System.out.println("【步骤1】添加 30"); + reverseSet.add(30); + System.out.println(" 树结构:"); + System.out.println(" 30 (根节点)"); + System.out.println(" / \\"); + System.out.println(" null null"); + System.out.println(" 当前集合: [30]"); + System.out.println(); + + // 第2步:添加 10 + System.out.println("【步骤2】添加 10"); + System.out.println(" 比较: compare(10, 30) = Integer.compare(30, 10) = 正数"); + System.out.println(" 判定: 10 > 30,所以10放在30的右边"); + reverseSet.add(10); + System.out.println(" 树结构:"); + System.out.println(" 30"); + System.out.println(" / \\"); + System.out.println(" null 10"); + System.out.println(" 当前集合: [30, 10]"); + System.out.println(); + + // 第3步:添加 20 + System.out.println("【步骤3】添加 20"); + System.out.println(" 第1次比较: compare(20, 30) = Integer.compare(30, 20) = 正数"); + System.out.println(" → 20 > 30,往右走"); + System.out.println(" 第2次比较: compare(20, 10) = Integer.compare(10, 20) = 负数"); + System.out.println(" → 20 < 10,往左走"); + reverseSet.add(20); + System.out.println(" 树结构:"); + System.out.println(" 30"); + System.out.println(" / \\"); + System.out.println(" null 10"); + System.out.println(" /"); + System.out.println(" 20"); + System.out.println(" 当前集合: [30, 20, 10] ← 注意!这是中序遍历的结果"); + System.out.println(); + + // 第4步:添加 40 + System.out.println("【步骤4】添加 40"); + System.out.println(" 第1次比较: compare(40, 30) = Integer.compare(30, 40) = 负数"); + System.out.println(" → 40 < 30,往左走"); + reverseSet.add(40); + System.out.println(" 树结构:"); + System.out.println(" 30"); + System.out.println(" / \\"); + System.out.println(" 40 10"); + System.out.println(" /"); + System.out.println(" 20"); + System.out.println(" 当前集合: [40, 30, 20, 10]"); + System.out.println(); + + // 第5步:添加 25 + System.out.println("【步骤5】添加 25"); + System.out.println(" 第1次比较: compare(25, 30) = Integer.compare(30, 25) = 正数"); + System.out.println(" → 25 > 30,往右走"); + System.out.println(" 第2次比较: compare(25, 10) = Integer.compare(10, 25) = 负数"); + System.out.println(" → 25 < 10,往左走"); + System.out.println(" 第3次比较: compare(25, 20) = Integer.compare(20, 25) = 负数"); + System.out.println(" → 25 < 20,往左走"); + reverseSet.add(25); + System.out.println(" 树结构:"); + System.out.println(" 30"); + System.out.println(" / \\"); + System.out.println(" 40 10"); + System.out.println(" /"); + System.out.println(" 20"); + System.out.println(" /"); + System.out.println(" 25"); + System.out.println(); + + // 输出最终结果 + System.out.println("✅ 最终红黑树结构:"); + System.out.println(" 30"); + System.out.println(" / \\"); + System.out.println(" 40 10"); + System.out.println(" /"); + System.out.println(" 20"); + System.out.println(" /"); + System.out.println(" 25"); + System.out.println(); + + System.out.println("🔄 中序遍历过程(左→根→右):"); + System.out.println(" 1. 从根节点30开始"); + System.out.println(" 2. 遍历左子树40:"); + System.out.println(" - 40的左子树为空"); + System.out.println(" - 访问40 → 输出 [40]"); + System.out.println(" - 40的右子树为空"); + System.out.println(" 3. 访问根节点30 → 输出 [40, 30]"); + System.out.println(" 4. 遍历右子树10:"); + System.out.println(" - 先遍历10的左子树20:"); + System.out.println(" · 先遍历20的左子树25:"); + System.out.println(" » 25的左子树为空"); + System.out.println(" » 访问25 → 输出 [40, 30, 25]"); + System.out.println(" » 25的右子树为空"); + System.out.println(" · 访问20 → 输出 [40, 30, 25, 20]"); + System.out.println(" · 20的右子树为空"); + System.out.println(" - 访问10 → 输出 [40, 30, 25, 20, 10]"); + System.out.println(" - 10的右子树为空"); + System.out.println(); + + System.out.println("📊 TreeSet实际输出结果:"); + System.out.println(" 添加顺序: 30, 10, 20, 40, 25"); + System.out.println(" 遍历结果: " + reverseSet); + System.out.println(); + + System.out.println("💡 关键理解:"); + System.out.println(" • 逆序的本质:通过反转比较规则,改变了树的构建方式"); + System.out.println(" • 大的数被放到左边,小的数被放到右边"); + System.out.println(" • 中序遍历仍然是'左→根→右',但因为左边是大数,所以得到降序"); + System.out.println(" • 这就是为什么 Integer.compare(o2, o1) 能实现逆序的原因"); + System.out.println(); + + System.out.println("🎯 对比升序和逆序:"); + System.out.println(" 升序: Integer.compare(o1, o2) → 小数在左 → 中序遍历得升序"); + System.out.println(" 逆序: Integer.compare(o2, o1) → 大数在左 → 中序遍历得降序"); + System.out.println(); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part13_MistakesAndSelection.java b/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part13_MistakesAndSelection.java index a33ff45..dd7e48c 100644 --- a/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part13_MistakesAndSelection.java +++ b/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part13_MistakesAndSelection.java @@ -33,30 +33,44 @@ static void explain() { System.out.println(" 真相:连接数少(<100)时,BIO 代码更简单,性能差不多"); System.out.println(" 连接数多(>1000)时,NIO 才有明显优势"); System.out.println(" 本质:BIO 瓶颈是线程数,NIO 瓶颈是 CPU 处理能力"); + System.out.println(" 小项目用 NIO 是过度设计,代码复杂但收益不大"); System.out.println(); System.out.println(" 误区2:以为 selector.select() 是忙等(CPU spin)"); System.out.println(" 真相:是真正的线程休眠(epoll_wait),该线程不占用 CPU 时间片"); + System.out.println(" 内核有事件时才唤醒线程,属于事件驱动,不是轮询"); System.out.println(); System.out.println(" 误区3:OP_WRITE 一直注册"); System.out.println(" 真相:发送缓冲区几乎一直有空间,OP_WRITE 几乎一直就绪"); System.out.println(" select() 一直立刻返回 → CPU 100%!"); System.out.println(" 正确:只在写不完时注册,写完立刻取消"); + System.out.println(" 类比:一直问“能发货吗”,快递员被你烦死了"); System.out.println(); System.out.println(" 误区4:selectedKeys() 不 remove()"); System.out.println(" 真相:已处理的 key 不自动移除,下次 select() 还会返回"); - System.out.println(" → 同一事件重复处理"); + System.out.println(" → 同一事件重复处理,数据错乱或重复发送"); + System.out.println(" 必须手动 iterator.remove(),否则逻辑混乱"); System.out.println(); System.out.println(" 误区5:FileChannel 以为能注册 Selector"); System.out.println(" 真相:FileChannel 不支持非阻塞模式,不能注册 Selector"); + System.out.println(" 磁盘 IO 是同步的,没有“就绪”概念,只能用阻塞方式读写"); System.out.println(); System.out.println(" 误区6:transferTo 不写 while 循环"); System.out.println(" 真相:网络场景一次可能传不完,不写 while 会静默丢数据"); + System.out.println(" 本地文件传输一般一次搞定,但网络传输必须循环检查"); + System.out.println(" 直到 transferred == count 才算完成"); System.out.println(); System.out.println(" 误区7:Netty handler 里做阻塞操作(数据库查询)"); System.out.println(" 真相:会把 EventLoop 线程卡死,整个服务停响"); System.out.println(" 解决:ctx.executor().execute(() → { 阻塞操作 → 回写 })"); System.out.println(" 或用 eventLoop.submit() 提交到业务线程池"); System.out.println(); + System.out.println(" ★ EventLoop 本质:一个线程 + 一个 Selector + 管理 N 个 Channel"); + System.out.println(" Netty 采用 Reactor 主从模式,Boss 线程负责 accept,Worker 线程负责读写"); + System.out.println(" 每个 Channel 绑定一个 EventLoop,同一 Channel 的事件始终在同一线程处理"); + System.out.println(" → 无锁化设计,无需 synchronized,这是高并发的关键"); + System.out.println(" → 一旦 handler 阻塞,该 EventLoop 负责的所有 Channel 都无法响应"); + System.out.println(" → 相当于一个快递员被绑在厕所,整片小区没人送快递"); + System.out.println(); System.out.println("二、CPU 拷贝 vs DMA 拷贝(容易混淆)"); System.out.println(); @@ -81,19 +95,19 @@ static void explain() { System.out.println("三、技术选型指南"); System.out.println(); - System.out.println(" ┌─────────────────────────┬────────────────────────────────────┐"); - System.out.println(" │ 场景 │ 推荐方案 │"); - System.out.println(" ├─────────────────────────┼────────────────────────────────────┤"); - System.out.println(" │ 读写本地小文件 │ BufferedInputStream/OutputStream │"); - System.out.println(" │ │ 或 Files.readAllBytes(< 64MB) │"); - System.out.println(" │ 大文件随机读取(GB级) │ MappedByteBuffer(内存映射) │"); - System.out.println(" │ 本地文件传输到网络 │ FileChannel.transferTo(零拷贝) │"); - System.out.println(" │ 自己写 HTTP/TCP 服务 │ Netty(不要用原生NIO,太复杂) │"); - System.out.println(" │ 微服务 RPC │ Dubbo/gRPC(底层都是 Netty) │"); - System.out.println(" │ 消息队列 │ Kafka(零拷贝+顺序写) │"); - System.out.println(" │ 高并发 Web 静态资源 │ Nginx(sendfile) │"); - System.out.println(" │ 高并发 Web 动态接口 │ Spring Boot(底层 Netty/Undertow) │"); - System.out.println(" └─────────────────────────┴────────────────────────────────────┘"); + System.out.println(" ┌─────────────────────────┬────────────────────────────────────┬─────────────────────────────────────┐"); + System.out.println(" │ 场景 │ 推荐方案 │ 原因 │"); + System.out.println(" ├─────────────────────────┼────────────────────────────────────┼─────────────────────────────────────┤"); + System.out.println(" │ 读写本地小文件 │ BufferedInputStream/OutputStream │ Buffer 缓冲够用,代码简单性能好 │"); + System.out.println(" │ │ 或 Files.readAllBytes(< 64MB) │ │"); + System.out.println(" │ 大文件随机读取(GB级) │ MappedByteBuffer(内存映射) │ mmap 映射到内存,随机访问像数组一样快 │"); + System.out.println(" │ 本地文件传输到网络 │ FileChannel.transferTo(零拷贝) │ 数据直接从 Page Cache 通过 DMA 发出 │"); + System.out.println(" │ 自己写 HTTP/TCP 服务 │ Netty(不要用原生NIO,太复杂) │ 原生 NIO 七大坑,Netty 全部封装好了 │"); + System.out.println(" │ 微服务 RPC │ Dubbo/gRPC(底层都是 Netty) │ 序列化、服务发现、负载均衡都内置了 │"); + System.out.println(" │ 消息队列 │ Kafka(零拷贝+顺序写) │ 顺序写 + sendfile,吞吐量 GB/s 级别 │"); + System.out.println(" │ 高并发 Web 静态资源 │ Nginx(sendfile) │ 静态文件直接从磁盘 DMA 到网卡 │"); + System.out.println(" │ 高并发 Web 动态接口 │ Spring Boot(底层 Netty/Undertow) │ Netty 处理连接 + Spring 开发效率高 │"); + System.out.println(" └─────────────────────────┴────────────────────────────────────┴─────────────────────────────────────┘"); System.out.println(); System.out.println("四、整体知识体系一张图"); diff --git a/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part14_FileChannelLocalIO.java b/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part14_FileChannelLocalIO.java index 94fc3bf..2a6f282 100644 --- a/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part14_FileChannelLocalIO.java +++ b/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part14_FileChannelLocalIO.java @@ -1,8 +1,24 @@ package org.example.java_base_test.io.nio.show_multi_agent; import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.nio.file.Files; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.RandomAccessFile; +import java.nio.channels.SeekableByteChannel; +import java.util.Set; +import java.nio.file.attribute.PosixFilePermission; -class Part14_FileChannelLocalIO { +public class Part14_FileChannelLocalIO { + + public static void main(String[] args) throws Exception { + demonstrate(); + } static void demonstrate() throws Exception { System.out.println("【第十四部分:FileChannel 本地文件 IO 完整操作】"); @@ -55,6 +71,12 @@ static void demonstrate() throws Exception { System.out.println(" FileChannel writeCh = new FileOutputStream(file).getChannel();"); System.out.println(" FileChannel rwCh = new RandomAccessFile(file, \"rw\").getChannel();"); System.out.println(); + System.out.println(" // 方式三:使用 Files.newByteChannel() 返回 SeekableByteChannel"); + System.out.println(" SeekableByteChannel sbc = Files.newByteChannel(path, options);"); + System.out.println(" if (sbc instanceof FileChannel) {"); + System.out.println(" FileChannel fc = (FileChannel) sbc;"); + System.out.println(" }"); + System.out.println(); System.out.println(" ⚠ 注意:关闭 Channel 会自动关闭关联的 Stream,反之亦然"); System.out.println(" Stream 和 Channel 不要各自关,关一个就够了"); System.out.println(); @@ -110,7 +132,7 @@ static void demonstrate() throws Exception { java.nio.file.Path tmpFile2 = java.nio.file.Files.createTempFile("fc_pos_", ".txt"); try { - // 先写 "ABCDEFGHIJ"(10字节) + // 先写 "ABCDE FGHIJ"(10字节) try (java.nio.channels.FileChannel fc = java.nio.channels.FileChannel.open(tmpFile2, java.nio.file.StandardOpenOption.WRITE, @@ -203,7 +225,7 @@ static void demonstrate() throws Exception { System.out.println(" 场景:两个 JVM 进程同时写同一个文件 → 数据损坏"); System.out.println(" FileLock 是操作系统级别的锁,不同进程都能感知"); System.out.println(" 注意:同一个 JVM 内多线程用 FileLock 无效,要用 synchronized"); - System.out.println(); + System.out.println( ); System.out.println(" 两种锁:"); System.out.println(" 独占锁(写锁):fc.lock() → 其他进程的 lock() 会阻塞"); System.out.println(" 共享锁(读锁):fc.lock(0, Long.MAX_VALUE, true)"); diff --git a/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part16_NIO2FilesAndWatch.java b/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part16_NIO2FilesAndWatch.java index 635f5bc..6327192 100644 --- a/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part16_NIO2FilesAndWatch.java +++ b/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part16_NIO2FilesAndWatch.java @@ -36,12 +36,12 @@ static void demonstrate() throws Exception { java.nio.file.Path p = java.nio.file.Paths.get("/Users/demo/data/test.txt"); System.out.println(" Path p = Paths.get(\"/Users/demo/data/test.txt\")"); - System.out.println(" p.getFileName() = " + p.getFileName()); - System.out.println(" p.getParent() = " + p.getParent()); - System.out.println(" p.getRoot() = " + p.getRoot()); - System.out.println(" p.getNameCount() = " + p.getNameCount() + " (路径分量数)"); - System.out.println(" p.getName(1) = " + p.getName(1) + " (第2段路径)"); - System.out.println(" p.isAbsolute() = " + p.isAbsolute()); + System.out.println(" p.getFileName() = " + p.getFileName()); // 获取文件名:test.txt(最后一段) + System.out.println(" p.getParent() = " + p.getParent()); // 获取父目录:/Users/demo/data + System.out.println(" p.getRoot() = " + p.getRoot()); // 获取根目录:/(Unix系统) + System.out.println(" p.getNameCount() = " + p.getNameCount() + " (路径分量数)"); // 3个分量:Users、data、test.txt + System.out.println(" p.getName(1) = " + p.getName(1) + " (第2段路径)"); // 索引从0开始:0=Users, 1=data, 2=test.txt + System.out.println(" p.isAbsolute() = " + p.isAbsolute()); // true:以 / 开头的是绝对路径 System.out.println(); // resolve:拼接路径 diff --git a/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part17_NettyArchitecture.java b/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part17_NettyArchitecture.java index 8b0f72d..8ffbdb6 100644 --- a/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part17_NettyArchitecture.java +++ b/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part17_NettyArchitecture.java @@ -55,6 +55,36 @@ static void explain() { System.out.println(" │ ⑦ │ 无心跳机制 │ IdleStateHandler │"); System.out.println(" └────┴─────────────────────────────┴──────────────────────────────┘"); System.out.println(); + System.out.println(" 【各痛点详解】"); + System.out.println(); + System.out.println(" ① ByteBuffer flip/clear 易错"); + System.out.println(" 原因:读写共用 1 个 position 指针,写完必须 flip() 才能读,忘了就读到空数据"); + System.out.println(" 解决:ByteBuf 用 readerIndex + writerIndex 双指针,读写独立,永远不需要 flip"); + System.out.println(); + System.out.println(" ② 粘包/拆包需自己处理"); + System.out.println(" 原因:TCP 是流式协议,无消息边界,多个包可能粘在一起或一个包被拆成多段"); + System.out.println(" 解决:内置 LineBasedFrameDecoder/LengthFieldBasedFrameDecoder 等按规则拆包"); + System.out.println(); + System.out.println(" ③ JDK epoll 空轮询 Bug"); + System.out.println(" 原因:Linux epoll 在连接异常关闭时,select() 无事件也立即返回,死循环 CPU 100%"); + System.out.println(" 解决:Netty 检测连续空轮询超 512 次,自动销毁旧 Selector 并重建新的"); + System.out.println(); + System.out.println(" ④ 异常处理分散繁琐"); + System.out.println(" 原因:每个 read()/write()/accept() 都要 try-catch,异常处理散落各处易遗漏"); + System.out.println(" 解决:Pipeline 中任何 Handler 抛异常自动传播到 exceptionCaught() 统一兜底"); + System.out.println(); + System.out.println(" ⑤ 无连接池"); + System.out.println(" 原因:原生 NIO 每次连接都创建新对象,频繁创建/销毁造成大量 GC 开销"); + System.out.println(" 解决:Netty 内部用 Recycler 对象池,Channel/ByteBuf 等核心对象可复用"); + System.out.println(); + System.out.println(" ⑥ 无编解码"); + System.out.println(" 原因:原生 NIO 只给原始 byte[],HTTP/WebSocket 等协议解析都要从零手写"); + System.out.println(" 解决:内置 HttpServerCodec/WebSocketHandler/ProtobufDecoder 等开箱即用"); + System.out.println(); + System.out.println(" ⑦ 无心跳机制"); + System.out.println(" 原因:TCP 连接可能「假死」(拔网线/对方宕机),应用层无感知,死连接占资源"); + System.out.println(" 解决:IdleStateHandler 一行配置读/写/读写空闲超时,触发心跳包或断连"); + System.out.println(); System.out.println(" 类比:原生 NIO 是「毛坯房 + 一堆砖头」,Netty 是「精装修公寓」"); System.out.println(" 你进去就可以住(写业务),不用自己砌墙(处理底层细节)"); System.out.println(); @@ -209,6 +239,10 @@ static void explain() { System.out.println(" Unpooled.xxx() 非池化,用完就丢,GC 回收"); System.out.println(" PooledByteBufAllocator.DEFAULT.buffer() 池化,借出用完还回去"); System.out.println(" Netty 默认:堆外 + 池化(高并发下大幅减少 GC 压力)"); + System.out.println(" 堆外原理:GC 通过引用链追踪对象,堆外让引用指向小对象(几十字节)"); + System.out.println(" 而非大数组(几MB),标记/回收工作量降低百倍,暂停时间从秒级降至毫秒级"); + System.out.println(" 池化好处:预先分配大块内存切分复用,避免频繁系统调用(allocateDirect 极慢)"); + System.out.println(" 对象复用不产生垃圾,减少 GC 频率,分配速度从微秒级提升至纳秒级"); System.out.println(); System.out.println(" ⚠ 内存泄漏陷阱:ByteBuf 是引用计数的(refCnt)"); System.out.println(" 创建时 refCnt = 1,调用 release() → refCnt - 1 → 归零则回收"); @@ -222,28 +256,35 @@ static void explain() { // ════════════════════════════════════════════════════════════════ System.out.println("━━━ 5. 从「客户端发数据」到「Handler 收到」的完整流程 ━━━━━━━"); System.out.println(); - System.out.println(" Step1:客户端发送数据包"); - System.out.println(" 网卡收到数据 → DMA 写入 Socket 接收缓冲区"); - System.out.println(" 硬件中断 → 内核唤醒 epoll_wait → Selector.select() 返回"); + System.out.println(" 📦 生活类比:把 Netty 处理请求想象成「餐厅接待顾客」"); + System.out.println(); + System.out.println(" Step1:顾客进店(客户端发送数据包)"); + System.out.println(" 顾客走到门口 → 门铃响起(网卡收到数据)"); + System.out.println(" 服务员听到铃声(硬件中断),注意到有新顾客(Selector.select() 返回)"); System.out.println(); - System.out.println(" Step2:EventLoop 线程处理 OP_READ 事件"); - System.out.println(" channel.read(ByteBuf) ← 把数据从 Socket 缓冲区搬到 ByteBuf"); - System.out.println(" 触发 pipeline.fireChannelRead(ByteBuf)"); + System.out.println(" Step2:接待员迎接(EventLoop 处理 OP_READ 事件)"); + System.out.println(" 接待员把顾客带到等候区(channel.read → 数据从 Socket 搬到 ByteBuf)"); + System.out.println(" 喊一声:'有客人来了!'(触发 pipeline.fireChannelRead)"); System.out.println(); - System.out.println(" Step3:ByteBuf 流过 Pipeline(Inbound 方向)"); - System.out.println(" [Head] → [LengthFieldDecoder] → [ProtobufDecoder] → [BusinessHandler]"); - System.out.println(" 每个 Handler 处理完调用 ctx.fireChannelRead(msg) 传给下一个"); + System.out.println(" Step3:流水线处理(ByteBuf 流过 Pipeline - Inbound 方向)"); + System.out.println(" [接待员] → [翻译员] → [厨师] → [经理]"); + System.out.println(" 接待员:记录顾客需求(LengthFieldDecoder 拆包)"); + System.out.println(" 翻译员:把方言翻译成普通话(ProtobufDecoder 解码)"); + System.out.println(" 厨师:做菜(BusinessHandler 业务处理)"); + System.out.println(" 每个环节做完,交给下一环节(ctx.fireChannelRead 传给下一个)"); System.out.println(); - System.out.println(" Step4:BusinessHandler 处理业务,写响应"); - System.out.println(" ctx.writeAndFlush(response) ← 触发 Outbound 方向"); + System.out.println(" Step4:经理准备回应(BusinessHandler 写响应)"); + System.out.println(" 经理说:'菜做好了,打包给顾客'(ctx.writeAndFlush)"); System.out.println(); - System.out.println(" Step5:响应流过 Pipeline(Outbound 方向)"); - System.out.println(" [BusinessHandler] → [ProtobufEncoder] → [LengthPrepender] → [Head]"); - System.out.println(" Head 将数据写入 Socket 发送缓冲区,网卡 DMA 发出"); + System.out.println(" Step5:打包送出(响应流过 Pipeline - Outbound 方向)"); + System.out.println(" [经理] → [打包员] → [贴标签员] → [外卖员]"); + System.out.println(" 打包员:装盒(ProtobufEncoder 编码)"); + System.out.println(" 贴标签员:写上地址(LengthPrepender 添加长度)"); + System.out.println(" 外卖员:送到顾客手上(Head 写入 Socket,网卡发出)"); System.out.println(); - System.out.println(" 整条链:"); - System.out.println(" 网卡 → Socket缓冲区 → ByteBuf → Pipeline(decode) → Handler"); - System.out.println(" Handler → Pipeline(encode) → Socket缓冲区 → 网卡 → 客户端"); + System.out.println(" 整条链总结:"); + System.out.println(" 顾客 → 门口 → 等候区 → 翻译 → 做菜 → 经理"); + System.out.println(" 经理 → 打包 → 贴标签 → 外卖员 → 顾客"); System.out.println(); NIODemo.printSeparator(); } diff --git a/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part18_NettyFrameDecoder.java b/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part18_NettyFrameDecoder.java index 7372ad7..2b2ef73 100644 --- a/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part18_NettyFrameDecoder.java +++ b/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part18_NettyFrameDecoder.java @@ -198,6 +198,69 @@ static void explain() { System.out.println(" }"); System.out.println(); NIODemo.printSeparator(); + + // ════════════════════════════════════════════════════════════════ + // 第五节:各方案特点对比总结 + // ════════════════════════════════════════════════════════════════ + System.out.println("━━━ 5. 各消息边界方案特点对比总结 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + System.out.println(); + System.out.println(" 【方案对比表】"); + System.out.println(" ┌────────────┬──────────┬──────────┬──────────┬───────────┐"); + System.out.println(" │ 方案 │ 变长支持 │ 性能 │ 复杂度 │ 适用场景 │"); + System.out.println(" ├────────────┼──────────┼──────────┼──────────┼───────────┤"); + System.out.println(" │ 固定长度 │ ❌ │ ⭐⭐⭐⭐⭐│ 极简 │ 定长协议 │"); + System.out.println(" │ 分隔符 │ ✅ │ ⭐⭐⭐ │ 简单 │ 文本协议 │"); + System.out.println(" │ 长度字段 │ ✅ │ ⭐⭐⭐⭐⭐│ 中等 │ 二进制协议│"); + System.out.println(" │ 自定义协议 │ ✅ │ ⭐⭐⭐⭐ │ 复杂 │ RPC框架 │"); + System.out.println(" └────────────┴──────────┴──────────┴──────────┴───────────┘"); + System.out.println(); + System.out.println(" 【详细对比】"); + System.out.println(); + System.out.println(" 1️⃣ 固定长度(FixedLengthFrameDecoder)"); + System.out.println(" 核心特点:每条消息必须严格等于 N 字节"); + System.out.println(" 优点:实现最简单,解码速度最快(无需解析头部)"); + System.out.println(" 缺点:消息短则浪费空间(需填充),消息长则被截断"); + System.out.println(" 典型应用:传感器数据上报、硬件通信协议"); + System.out.println(" 示例:GPS定位数据(每条固定32字节)"); + System.out.println(); + System.out.println(" 2️⃣ 分隔符(DelimiterBasedFrameDecoder / LineBasedFrameDecoder)"); + System.out.println(" 核心特点:用特殊字符(如\\n、\\r\\n)标记消息结束"); + System.out.println(" 优点:支持变长消息,人类可读,调试方便"); + System.out.println(" 缺点:消息内容不能包含分隔符(需转义),需扫描每个字节找分隔符"); + System.out.println(" 典型应用:HTTP/1.x、SMTP、FTP、Telnet 等文本协议"); + System.out.println(" 示例:HTTP请求头以\\r\\n\\r\\n结束"); + System.out.println(); + System.out.println(" 3️⃣ 长度字段(LengthFieldBasedFrameDecoder)← 工业界首选"); + System.out.println(" 核心特点:消息 = 长度头 + 消息体,先读长度再读body"); + System.out.println(" 优点:支持变长,内容可含任意字节(包括分隔符),性能最优"); + System.out.println(" 缺点:需预先知道长度字段的位置和大小,配置参数较多"); + System.out.println(" 典型应用:Dubbo、gRPC、Thrift、Kafka协议、Redis协议"); + System.out.println(" 示例:[4字节长度][N字节body],长度值=N"); + System.out.println(); + System.out.println(" 4️⃣ 自定义协议(ReplayingDecoder / ByteToMessageDecoder)"); + System.out.println(" 核心特点:完整的协议设计,包含魔数、版本、类型、长度等字段"); + System.out.println(" 优点:最灵活,可扩展性强,支持协议演进和兼容性"); + System.out.println(" 缺点:开发成本高,需自行处理编解码逻辑"); + System.out.println(" 典型应用:自研RPC框架、游戏服务器协议、金融交易系统"); + System.out.println(" 示例:[魔数4B][版本1B][类型1B][序列号4B][长度4B][body]"); + System.out.println(); + System.out.println(" 【选择建议】"); + System.out.println(); + System.out.println(" ✅ 选固定长度:消息长度固定且较短(如传感器数据)"); + System.out.println(" ✅ 选分隔符: 文本协议、人类可读、调试友好(如HTTP)"); + System.out.println(" ✅ 选长度字段:高性能二进制协议、通用场景(强烈推荐!)"); + System.out.println(" ✅ 选自定义: 需要协议版本控制、复杂业务逻辑(如RPC)"); + System.out.println(); + System.out.println(" 【性能对比(吞吐量)】"); + System.out.println(" 固定长度 > 长度字段 > 分隔符 > 自定义协议"); + System.out.println(" (但实际差距不大,长度字段方案已足够优秀)"); + System.out.println(); + System.out.println(" 【Netty 内置 Decoder 速查】"); + System.out.println(" FixedLengthFrameDecoder(1024) → 固定1024字节"); + System.out.println(" LineBasedFrameDecoder(8192) → 按行分割(\\n或\\r\\n)"); + System.out.println(" DelimiterBasedFrameDecoder(8192, delim) → 自定义分隔符"); + System.out.println(" LengthFieldBasedFrameDecoder(...) → 长度字段(6个参数)"); + System.out.println(" ReplayingDecoder / ByteToMessageDecoder → 自定义协议"); } } diff --git a/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part19_NettyPractice.java b/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part19_NettyPractice.java index 4d69281..00fe468 100644 --- a/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part19_NettyPractice.java +++ b/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part19_NettyPractice.java @@ -266,6 +266,61 @@ static void explain() { System.out.println(" // 第一个参数 quietPeriod=0:没有新任务时立即关闭"); System.out.println(" // 第二个参数 timeout=5:最多等 5 秒"); System.out.println(); + + // ════════════════════════════════════════════════════════════════ + // 第五节:核心知识点总结 + // ════════════════════════════════════════════════════════════════ + System.out.println("━━━ 5. 核心知识点总结(你需要掌握什么)━━━━━━━━━━━━━━━━━━━━━━"); + System.out.println(); + System.out.println(" 【第一优先级:必须理解】"); + System.out.println(); + System.out.println(" 1️⃣ Boss/Worker线程模型"); + System.out.println(" Boss:只负责accept新连接(类似餐厅前台,1个线程足够)"); + System.out.println(" Worker:负责IO读写+业务处理(类似服务员,CPU×2个线程)"); + System.out.println(" 为什么分离?提高并发性能,accept不阻塞IO处理"); + System.out.println(); + System.out.println(" 2️⃣ Pipeline和Handler链"); + System.out.println(" 数据像流水线一样依次经过各个Handler"); + System.out.println(" Inbound方向:拆包→解码→业务(从外到内)"); + System.out.println(" Outbound方向:业务→编码→发送(从内到外)"); + System.out.println(" 顺序很重要!拆包必须在解码之前"); + System.out.println(); + System.out.println(" 3️⃣ 粘包/拆包处理"); + System.out.println(" TCP是字节流,不保证消息边界"); + System.out.println(" 可能一次读到多条消息(粘包)或一条分多次(拆包)"); + System.out.println(" 必须定义消息边界:固定长度、分隔符、长度字段"); + System.out.println(" Netty内置Decoder:LineBased/Delimiter/LengthField"); + System.out.println(); + System.out.println(" 4️⃣ 内存管理(ByteBuf引用计数)"); + System.out.println(" SimpleChannelInboundHandler:自动release(推荐)"); + System.out.println(" ChannelInboundHandlerAdapter:手动release"); + System.out.println(" 忘记release会导致堆外内存泄漏→OutOfMemoryError"); + System.out.println(" 原则:谁最后用,谁release"); + System.out.println(); + System.out.println(" 【第二优先级:应该理解】"); + System.out.println(); + System.out.println(" 5️⃣ 异步编程(ChannelFuture)"); + System.out.println(" Netty所有IO操作都是异步的,立即返回Future"); + System.out.println(" ❌ 不要在EventLoop线程中调用sync()(会死锁)"); + System.out.println(" ✅ 使用addListener添加回调(推荐)"); + System.out.println(" 可用常量:ChannelFutureListener.CLOSE等"); + System.out.println(); + System.out.println(" 6️⃣ HTTP协议处理"); + System.out.println(" HttpRequestDecoder:字节→HttpRequest+HttpContent"); + System.out.println(" HttpObjectAggregator:聚合分片请求为FullHttpRequest"); + System.out.println(" HttpResponseEncoder:HttpResponse→字节"); + System.out.println(" Spring Boot底层就是这样工作的"); + System.out.println(); + System.out.println(" 7️⃣ 连接池"); + System.out.println(" TCP三次握手耗时~1ms,高并发下频繁建连接性能差"); + System.out.println(" 复用连接:借acquire() → 使用 → 还release()"); + System.out.println(" RPC框架(Dubbo/gRPC)都用连接池"); + System.out.println(" 借了必须还,否则连接泄漏"); + System.out.println(); + System.out.println(" 8️⃣ 优雅关闭"); + System.out.println(" 不要直接System.exit()!可能有未处理的请求"); + System.out.println(" shutdownGracefully(quietPeriod, timeout, unit)"); + System.out.println(" quietPeriod=0:无新任务立即关闭"); NIODemo.printSeparator(); } } diff --git a/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part1_FiveIOModels.java b/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part1_FiveIOModels.java index 18b7815..6a6959b 100644 --- a/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part1_FiveIOModels.java +++ b/src/main/java/org/example/java_base_test/io/nio/show_multi_agent/Part1_FiveIOModels.java @@ -17,21 +17,33 @@ static void explain() { System.out.println(); System.out.println(" 模型① BIO → 你打电话给前台:「我要一份外卖,好了叫我」"); System.out.println(" 然后你就站在前台等,什么都不干,直到外卖到了才走"); - System.out.println(" (线程全程阻塞挂起,无法服务其他连接,线程资源白白占用)"); + System.out.println(" (线程全程阻塞挂起,无法服务其他连接,线程资源白白占用)");//此时CPU转向执行其他线程,当前线程被阻塞。 + System.out.println(" 【关系】1:1(一对一绑定),一个线程独占一个连接"); + System.out.println(" 【表现】连接数增加→线程数必须线性增加,阻塞期间不占CPU但占内存"); System.out.println(); System.out.println(" 模型② NIO轮询 → 你回工位,每隔30秒起来跑一趟前台问:「外卖到了吗?」"); System.out.println(" 没到就回去,30秒后再跑一趟。如此反复。"); - System.out.println(" (CPU 一直忙着轮询,浪费在大量无效的「没到」)"); + System.out.println(" (CPU 一直忙着轮询,浪费在大量无效的「没到」)");//cpu轮询是因为线程需要不断知道数据是否就绪,于是线程主动调用方法,cpu去判断数据是否就绪。 + System.out.println(" 【关系】1:N(但无效消耗高),一个线程管理N个连接但高频无效检查"); + System.out.println(" 【表现】即使999个连接无数据,线程依然一次次去问,CPU空转"); System.out.println(); System.out.println(" 模型③ IO多路复用 → 外卖平台给你一个取餐器(振动手环),"); System.out.println(" 你回工位安心工作,多个外卖同时等,哪个振了去取哪个。"); System.out.println(" (Selector!1个线程监控N个连接,有就绪才处理)"); + System.out.println(" 【关系】M:N(M<