设计模式——外观:批量接口的价值
外观模式的公开描述是「为子系统提供统一接口」。这句话在评审里没有用,谁也说不清「统一」到什么粒度。换一个可测量的说法:外观把 N 次细粒度调用收成 1 次粗粒度调用,收益上限就是省下的那 N-1 次调用的固定成本。本文写一万条小记录:逐条裸写、套缓冲、内存拼装后单次写,用 write(2) 计数和两种 JDK 的耗时给这三条路的差价结账,再看 JDK 自己怎么做外观。 一、把「统一接口」换成调用次数外观模式的介绍通常画一张图:客户端指向一个 Facade 类,Facade 再指向三个子系统的类。图里没有数字,读完记不住任何可执行的东西。 把接口设计换成计费问题就具体了。每次调用都有一条固定成本:跨边界的开销。这个边界可能在进程之间、网络之间、内核与应用之间,也可能就在模块之间。外观把 N 次调用合并成 1 次,收益就是省下的 N-1 条固定成本;代价是这次合并本身要付参数打包、错误语义合并、内存占用的账。 写文件的例子把这两种成本都摆了出来。同样一万条记录、同样的字节,三种写法: 逐条 FileOutputStream.write(byte[]),每条一次系统调用。 套一层...
Docker 容器走代理:注入点、作用范围与实测踩到的坑
容器既不继承宿主 shell 里 export 的代理变量,也不继承系统代理设置。要把容器流量送进代理,只有有限的几个注入点,作用范围各不相同。这篇在 macOS(OrbStack 29.4.0,arm64)上把每个注入点单独跑了一遍,顺带记下几个会让人得出错误结论的坑。「请求成功」不能证明流量走了代理:很多环境本来就能直连,唯一可信的证据是代理侧的访问日志。下面的实验全部以代理日志为准,而不是看命令返回码。 1. 先造一把尺子:会记日志的代理要判断「有没有走代理」,得有一个只在被使用时才留下痕迹的东西。用一个 40 行的记录型 HTTP 代理就够:收到请求先把目标写进日志,再做正常的转发(CONNECT 直接打隧道,明文 HTTP 转发绝对 URI)。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950#!/usr/bin/env python3"""记录型 HTTP 代理:记录每条请求后正常转发,用于判断流量是否...
设计模式——组合:树与叶子的同一接口
组合模式把容器和叶子放在同一个接口后面,客户端写一个递归就能处理整棵树。代价落在三处:每个节点都要过一次接口调用,类型信息被接口抹平,递归深度受线程栈限制。本文用一棵 10 万节点的树、三种遍历写法、三组测量和两个 JDK,把这三笔账量一遍。 一、组合模式换到的东西树形结构里,同一段代码常要处理容器和叶子:目录与文件、窗口与按钮、JSON 对象与字面量。组合模式的做法是让容器实现与叶子相同的接口,容器持有同一类型的子节点数组。 classDiagram direction TB class Component { <<interface>> +value() int +children() Component[] } class Leaf class Container { -Component[] kids } Component <|.. Leaf Component <|.. Container ...
Java——ScopedValue 替代 ThreadLocal
传递”当前请求是谁发的”这类数据,过去的标准答案是 ThreadLocal。它在同步阻塞的世界里很好用,但遇到线程池和虚拟线程就开始漏水:值会跨请求残留、每个线程都要存一份、子线程继承要靠另一套 API。ScopedValue 在 JDK 25 转正(JEP 506,Status: Closed / Delivered、Release: 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) ScopedValue 在 JDK 25 已经是正式特性,不需要 --enable-preview;而同期的 StructuredTask...
设计模式——桥接:两个维度独立变化
两个维度各自独立变化时,继承把它们乘起来:4 种报表 × 4 种格式 = 16 个叶子类,加第 5 种报表要再写 4 个。桥接把乘积拆成和:两个层次各管一个维度,运行时用一个引用把交叉点接上。代价是热路径上每次调用多一跳。JIT 内联掉这一跳,它是零成本;内联不掉,就是每次 0.6 纳秒。 一、桥接换的是算术一个报表导出模块。报表有 4 种,导出格式有 4 种,两者的交叉决定行为:销售报表按 CSV 输出,和库存报表按 CSV 输出,是两个不同的实现,共享的只有「把逗号换成竖线」这类细节。 用继承写,交叉点就是类: classDiagram direction TB class Report { <<abstract>> +export(data) } class SalesCsv class SalesJson class SalesXml class SalesMarkdown class InventoryCsv class...
设计模式——适配器:边界在哪
适配器只做一件事:把调用方要的接口翻译成现有对象提供的接口,不改变被包装对象的行为。常见的反对意见是「多一层调用会拖慢热点路径」。JIT 打开、调用点单态时,这层开销量出来是零;调用点收到多个实现类型时,量出约三倍。两种情况本文都用 1 亿次调用测一遍,并贴出 -XX:+PrintInlining 对每一次内联决定的原文。 一、问题:那一层调用值多少钱手里有一个只能按字节取的类,调用方要的是一个按字符取的接口,中间补一个类: 1234567interface CharReader { int read(); } // 调用方要求的接口final class SourceAdapter implements CharReader { private final ByteSource src; // 已有的实现,改不了 SourceAdapter(ByteSource src) { this.src = src; } @Override public int read()...
Java——结构化并发实战(StructuredTaskScope)
《虚拟线程实战》解决的是”线程不再贵”,但没解决”谁来管这些线程”。一个请求扇出到三个下游,其中一个失败了怎么办?超时了剩下的两个还在跑吗?调用方抛出异常时,那些已经没人等待的任务去哪了?这些问题的答案,在 CompletableFuture、ExecutorService 里是”你自己记住”,在结构化并发里是”作用域负责”。这篇用同一份需求把三种写法跑一遍,用实测的耗时和线程残留数说话。全部代码在 JDK 25 上编译运行,StructuredTaskScope 目前仍是预览 API,编译需要 --enable-preview。 实验环境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) 编译与运行都要带预览开关: 12javac --enable-preview --release...
设计模式——享元:共享不可变状态
Integer.valueOf(500) 每次调用返回一个新对象,Integer.valueOf(5) 每次返回同一个。两者的方法体一样,差别在 IntegerCache 的区间上,而这个区间由启动参数决定。享元模式做的就这一件事:把不变的部分抽出来放进池子共享,把每次都不同的部分当参数传进去。Integer 的值是内在状态,调用方要哪个数是外在状态。1 亿次调用、一组 JVM 参数和两个 JDK 量出这条分界线的两个后果:分配字节从每次 16 变成 0,耗时只差 1.5 倍左右。差额的去处在 TLAB 和 GC 里。 一、享元拆开了什么享元把对象的字段拆成两半。实例之间重复的那部分字段,JDK 里的说法是内在状态,抽出来放进池子,每个值只留一个实例。随调用变化的那部分叫外在状态,留在对象外面,每次调用时当参数传进来。 classDiagram direction LR class FlyweightFactory { -Map pool +get(Key) Flyweight } class Flyw...
设计模式——代理:动态代理的成本与边界
代理保持调用方手里的类型不变,中间多插一层。日志、鉴权、重试、延迟加载、远程调用都落在这一层上。手写静态代理的问题是类数按「接口数 × 关注点数」涨,每个类里都是逐方法转发的样板。Proxy.newProxyInstance 把这件事挪到运行时:一次调用生成一个实现全部接口的类,一个 InvocationHandler 接管所有方法。代价在生成的字节码里:每个方法开头一次 anewarray,每个基本类型参数一次装箱。下面用 1 亿次调用把这份代价量出来,再单独量类生成与缓存。 一、代理解决的问题静态代理的算术三个接口、四个关注点。日志、鉴权、重试、指标,每个都要包一层,静态写法就得写这么多个类: classDiagram direction LR class OrderService class UserService class PayService class LoggingOrder class AuthOrder class RetryOrder class MetricsOrder ...
Java——容器里的 JVM:内存、CPU 与镜像
这个博客里有两条线:《Ubuntu LTS 中安装 Docker 和 Docker Compose》站在容器外面看机器,《JVM 内存与 GC 定位实战》站在 JVM 里面看内存。这篇是它们的交界处,回答一个很朴素的问题:同一个 jar,在宿主机上和在容器里,JVM 看到的”机器”是不是同一台?不是同一台。默认堆大小、可用核数、甚至 GC 的选择都会变,而且这些变化是静默的——代码不报错,只是行为和你在本机测出来的不一样。下面每个数字都对应 JDK 25 与 Docker 的真实输出。 实验环境12345678910$ 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)$ docker version --format '{{.Server.Version}&...







