设计模式——命令:撤销栈与队列化
doc.insert(pos, text) 编译之后只剩一条 invokevirtual,方法一返回,这次调用就没有任何痕迹了。命令模式把这一行换成 new InsertCmd(doc, pos, text),这次调用变成内存里的一个值:能入栈做撤销、入队做排队、写盘做重放。10 万次操作、四种形态,量的是耗时、分配字节与延迟,以及撤销栈里「记快照」和「记反向操作」两条路的内存差。 一、把调用变成对象一个方法调用在编译后活在栈帧里。接收者、参数、要做什么,都散在字节码的指令序列上,方法一旦返回,这些信息就没了。要撤销它,得在调用之前自己把原位字符记下来;要排队,得把它包进一个 lambda 交给别人;要记录,得在旁边再写一行日志。 命令模式把这一行换成 new InsertCmd(doc, pos, text):调用发生的时间和执行的时间分开,这次调用就成了内存里的一个值。 撤销、排队、记录都建立在这个值上: 撤销:入栈,回滚时按逆序对每条执行相反的操作。 排队:入队,由另一个线程按自己的节奏取走执行。 记录与重放:写进日志或消息,读回来再执行一遍。 把调用变成对象,代价...
设计模式——策略:有状态与无状态
策略模式把「用哪个算法」从调用点挪开,代价跟着一起挪,落在别的地方。一份共享的无状态实例和直接调用测不出差别;每次新建带状态的策略,成本从调用换成分配;从表里取策略,成本落在查表之后那个无法内联的调用点上。这篇在 JDK 21.0.8 和 JDK 25 上各跑 1 亿次调用,量四种形态的耗时和分配字节。 一、成本落在哪两个地方一个策略调用点要回答两件事:这次执行的是谁(分派),它需要的状态从哪来(持有或分配)。 分派和分配的价格差了好几个量级。分派如果是单态的,JIT 能把接口调用消成一条直接调用,再和调用体一起内联进循环,循环随即被向量化,最后测出来是每调用 0.3 纳秒这个量级。分配只要是逃逸的,每次调用都要付出一个对象的内存,1 亿次就是 2.4 GB 的垃圾。 策略有状态还是无状态,决定成本落在分派还是分配上。 graph TD CALL["调用点 p.apply(x)"] --> SEL{"p 从哪里来"} SEL -->|"static final,一份"| S1[&...
设计模式——状态:状态位还是状态类
FutureTask 管一个任务从提交到结束的全部状态,字段是 volatile int state,七个常量,靠 CAS 迁移。ExecutorService 和 CompletableFuture 都建在它上面,JDK 没给它写七个状态类。GoF 的状态模式走另一条路:一个状态一个类,事件交给多态处理,状态自己决定下一步。书上那个 TCP 连接例子好写,代价没有现成数字。本文把同一个四状态状态机写成五种形式,在 JDK 21 与 25 上各跑 5033 万次事件,量耗时与分配,再用字节码和 JIT 日志解释差距从哪来。 一、状态放在哪里状态的存储方式和迁移的决策方式可以拆开看。GoF 把两件事绑在一起:状态是一个对象,迁移由虚调用决定。JDK 里常见的是另一种组合:状态是一个 int,迁移是一串比较和位运算。 两边的差别用一台四状态、五事件的状态机就能摆出来,后面所有实验共用它: stateDiagram-v2 [*] --> NEW NEW --> RUN : START NEW --> FAIL : CANCEL ...
设计模式——备忘录:快照的代价
备忘录把「回到过去」变成两个方法调用:save() 存下一版,restore() 回到某一版。存一版花多少时间、占多少内存,模式定义里没有答案。全量深拷贝、增量日志、写时拷贝三条路都能实现回滚,代价相差两个数量级。本文用 1 万条记录(含标签集合)的状态、1000 个事务、每事务改 10 条,量三条路的每事务耗时与分配字节,再量历史上限 10/100/1000 版时的保留内存和回滚耗时。 一、回滚能力买的是什么发起人(Originator)把状态装进一个备忘录对象,管理者(Caretaker)把备忘录收进版本链,需要时再把某一版交回发起人恢复。快照粒度、版本存放位置、版本何时失效,这些都不在模式定义里。 三个问题决定价格: 存的是全部状态,还是这一版改动的增量。 版本链保留多少版。 代价在存的那一刻付,还是在每次改动时付。 第一个问题把实现分成三条路,后两个问题决定账单什么时候来。 三条路全量深拷贝:save() 把当前状态整份复制一份挂到链上,回滚时把复制出来的那份换回来。 增量日志:save() 只记一个位置,每次改动之前把旧值写进日志,回滚时按逆...
设计模式——中介者:谁负责协调
中介者把参与者之间的 N(N−1) 条引用换成 N 条,代价是所有人的调用都要经过它。这笔代价有一半是虚构的:JIT 把只做转发的分发路径内联之后,它和直连编译成同一段代码,测不出差别。另一半是真的,来自中介者自己持有的共享状态:一个轮转游标、一个全局序号、一把锁。 一、换掉的是引用,也是决定权8 个参与者互相发事件。直连的写法是每个参与者持有其余 7 个的引用;中介者的写法是每个参与者只认识中介者。 graph TB subgraph d["直连:参与者两两持有引用,8 个节点 56 条边"] direction LR D1((A)) --- D2((B)) D1 --- D3((C)) D1 --- D4((D)) D2 --- D3 D2 --- D4 D3 --- D4 end subgraph m["中介者:每个参与者一条引用,加上中介者的 8 条反向引用共 16 条"] ...
设计模式——解释器:解释还是编译
解释器模式把一门语言的求值写成对象树上的递归。同一棵树可以在构建期换成闭包树,每个节点变成一个捕获了子闭包的函数。两种形态的求值都是每个节点一次调用,差别落在调用点能不能被 JIT 静态化。用 27 节点和 7 节点两个表达式、各 100 万次求值,可以把这句话量出来。 一、两个求值器四则运算的最小文法:数字、变量、加减乘除。节点是一组 record,求值是节点自己的方法: 12345678sealed interface Node { double eval(double[] v); }record Num(double k) implements Node { public double eval(double[] v) { return k; } }record Var(int i) implements Node { public double eval(double[] v) { return v[i]; } }record Add(Node l, Node r) i...
sing-box 客户端 TUN 落地:配置、分流验证与 1.14 拒收的旧写法
客户端上的 TUN 模式和「代理模式」的差别在于接管范围:代理模式只服务主动配置了代理的应用,TUN 模式把整机流量都交给 sing-box。代价是必须处理路由、DNS 与分流,而且这三个东西经常出问题,所以验证方法比配置本身更重要。这篇在一台一次性 Ubuntu 虚拟机(arm64)上用 sing-box 1.14.1 跑完整套接管:TUN 接口与路由的变化、DNS 劫持、用一条 reject 规则证明分流生效、退出后的自动回收,以及 1.14 会直接拒绝启动的三处旧写法原文报错。 1. 便宜的说法与真实的代价服务端那篇讲的是「在一台服务器上暴露代理」;客户端这边解决的是另一个问题:本机有一堆程序,浏览器、终端、Docker、后台服务,逐个配代理既麻烦又会漏。TUN 的做法是造一个虚拟网卡,把默认路由指过去,让内核把流量交给 sing-box 决定怎么走。 听上去只是「接管」,但要同时成立四件事: 路由要从物理网卡切到 TUN,且不能把 sing-box 自己的出站流量也绕回去(回环) DNS 要一起接管,否则域名解析仍然暴露在明网,分流规则也无从匹配 分流规则要能验证:...
设计模式——责任链:三种形态
责任链把「谁处理这个请求」从调用点挪到了运行时。调用方发出一次,节点自己决定接不接。JDK 里这条链有三种落地形态,表达「拦住了」的方式各不相同:处理链返回 boolean,过滤链返回替换对象或 null,Stream 的 stage 链用反向询问的取消标记。本文用 1 亿个元素量三种形态的每元素成本随链长的变化,用 8 节点链量命中位置的影响,再从 JDK 源码指出差别出自哪一行。 一、三种形态,三种收场一个请求要穿过若干检查点,每个检查点各自判断:这个我能处理,或者这不归我管,交给下一个。责任链(Chain of Responsibility)把这条传递路径做成对象结构,但「传递」本身在 JDK 里有三条不同的实现路线。 处理链的节点返回 boolean。命中返回 true,调用方知道事情办完了;全部返回 false 就是没人管。停下来的信号沿返回值传回调用方。 过滤链的节点不报告「办没办成」,返回一个替换对象或者 null。返回新对象表示这一层改写了请求、需要重发;返回 null 表示这一层放行,继续看下一个节点。java.net.http 用这个形状:请求过滤器正序遍...
设计模式——观察者:从 Observable 到背压
观察者模式在代码层面只有十几行:一个注册表,一次遍历回调。订阅者比发布者快时,这个形态没有可测的代价;慢订阅者占住的是发布者的线程。JDK 1.0 给了 Observable,JDK 9 把它废弃,同一个版本里给出 Flow:四个接口,补上的那一块是 Subscription.request(long)。 一、228 行的废弃类java.util.Observable 从 JDK 1.0 就在。JDK 25 的 src.zip 里它还在,228 行,第 75 行写着: 12@Deprecated(since="9")public class Observable { 两个 LTS 过去了,JDK 一直没把它移除。javadoc 给的理由(java.base/java/util/Observable.java:62-74): 123456789101112* @deprecated* This class and the {@link Observer} interface have been deprecated.* The...
Java——Stream Gatherers 自定义管道
《Stream API 常见误用》讲的是内建操作怎么用错;这篇讲内建操作不够用时怎么办。过去要在流中间做”滑动窗口””按状态切分””限流并发”这类事,答案通常是”收集成 List 再用 for 循环”,或者硬塞一个 Collector——后者只能在终结处用,中间管道依然插不进去。JDK 24 转正的 JEP 485 补上了这块:Stream.gather(Gatherer) 是中间操作,和 map、filter 处在同一个位置上。全部代码在 JDK 25 上编译运行,不需要预览开关。 实验环境1234$ java -versionjava 25 2025-09-16 LTSJava(TM) SE Runtime Environment (build 25+37-LTS-3491)Java HotSpot(TM) 64-Bit Server VM (build 25+37-LTS-3491, mixed mode, sharing) 一、Gatherer 在管道里的位置JEP 485 的定位说得很清楚: Stream::gather(Gatherer) is to inte...








