设计模式——单例的五种写法与代价
概述 单例(Singleton)是被讲得最多、也被误用最多的设计模式之一。这篇只回答三个问题: 五种写法各自把代价付在了哪里?用 JDK 25 的字节码和实测延迟回答。 “唯一性”能不能被打破?实跑反射、反序列化、Unsafe、clone 四条路径,包括对枚举版的攻击。 什么时候不该用单例?全局状态、测试隔离、类加载器边界。 代码均在本机 JDK 21 与 JDK 25 编译运行,txt 块是真实终端输出;涉及线程语义的结论都指到 JLS 具体章节。 五种写法每种实现的构造器里都调用一次 Work.expensive():一段 4000 万次的整数忙循环(for (int i = 0; i < 40_000_000; i++) x = (x * 31 + i) ^ (x >>> 5);,最后返回 x),在本机约 75ms,好让”初始化成本”在实测里被定位到不同时间点。 一、饿汉式:类初始化时就把实例建好12345678910111213public final class EagerSingleton { // 类初始化时(JLS ...
设计模式——装饰器:缓冲链的代价
装饰器把「给一个对象加能力」变成了廉价操作:new BufferedInputStream(new FileInputStream(f)) 只是多写一层。但每一层都会改写下层被调用的形态——调用次数、每次的块大小、flush 与 close 的语义。这些改动在源码里看不到,只能测。本文用一个 48 MiB 文件、libc 层的真实 read(2) 计数,和两个 JDK(21.0.8 与 25)回答一个问题:这条链上多包一层,到底买到了什么,代价在哪。 一、装饰器换到的东西继承式的算术给一个数据源加「缓冲」和「校验」两种能力。如果用继承,每多一种能力就要多一层子类,而两种能力的叠加顺序又各自是一个类: classDiagram direction TB class FileDataSource class BufferedFile class ChecksumFile class BufferedChecksumFile class ChecksumBufferedFile FileDataSource <...
设计模式——被语言吃掉的那些模式
GoF 那本书写于 1994 年,示例语言是 C++ 与 Smalltalk。Java 要到 1995 年才发布,lambda 要等到 2014 年。23 种模式里有一部分在补语言的窟窿:用接口加实现类代替一等函数,用抽象方法代替传入函数,用两次分派代替模式匹配,用建造者代替命名参数。这篇文章把其中六组摆在一起对照,并且跑三组实验:Visitor、Strategy、Template Method,在 JDK 21.0.8 与 JDK 25 上各测一遍。数据里有个反直觉的结果——写得短的那个版本在两个 JDK 上都没有更快,Visitor 那一组甚至慢了 17.8%。测量方法与环境写在第一节末尾。 1994 年缺的那些东西GoF 的示例用 C++ 写成,Smalltalk 补充。当时的语言没有这些东西: 一等函数(Java 8,JSR 335 引入 lambda 与方法引用) 语言的 for-each(Java 5 才有,靠 Iterable 支撑) record(JEP 395,JDK 16 转正) sealed 继承层次(JEP 409,JDK 17 转正) switch...
设计模式——不可变与防御性拷贝
不可变有两个动作:造的时候不让别人改,交出去的时候把边界封住。共享和拷贝是两笔账:共享把成本压在构造那一刻,拷贝把成本摊到每一次读取上。1 亿次读取、1024 元素的列表、一组布尔验证,把这两笔账分开算,跑在两个 LTS 上:JDK 21.0.8 与 JDK 25。 一、两笔账记在谁头上对象内部持有 1024 字节的数组,上层有很多只读的调用方,写法有两种。 123456789static final class SharedRepo { private final int[] data = new int[256]; int[] data() { return data; } // 交出引用}static final class CopyRepo { private final int[] data = new int[256]; int[] data() { return Arrays.copyOf(data, data.length); ...
Java——并发工具选型:从线程池到结构化并发
这个博客的并发文章已经有五篇:《虚拟线程实战》讲它什么时候有用、《虚拟线程迁移实测》给出改造前后的数字、《一次锁竞争的完整定位》讲出事怎么查、《结构化并发实战》讲谁负责善后、《ScopedValue 替代 ThreadLocal》讲上下文怎么传。它们各自回答了一个问题,却没人回答那个每天都要回答的问题:这段代码该用哪件工具。这篇就是那张对照表:请求扇出、CPU 密集计算、超时与取消、批处理限流、上下文传递,每个推荐都附「什么时候不要用」和出处;唯一新加的实验只有 4 秒。版本基线是 JDK 25(LTS):虚拟线程(JEP 444)与 ScopedValue(JEP 506)已转正,StructuredTaskScope 仍是预览 API,编译运行都要 --enable-preview,形态是 open() + Joiner——new StructuredTaskScope.ShutdownOnFailure() 那套是 JDK 21/24 的写法,在 25 上编译不过。 一张决策表 场景 首选 理由(实测) 什么时候不要用 单请求扇出 2–10 个下游,...
设计模式——对象池:什么时候是负优化
对象池把「分配」换成了「借还」。分配走线程私有的 TLAB,借还抢所有线程共用的队列。收益只能来自免掉构造与初始化,代价是同步。构造越贵,这笔交换越划算;构造越便宜,省下的越少,而同步开销照付。三种构造成本(1 KiB 数组、DirectByteBuffer(1 KiB)、SecureRandom 初始化)乘三种取法(每次 new、ThreadLocal 池、ReentrantLock 池),在 1、8、16 线程下各计时 1 秒,JDK 21 与 JDK 25 都跑,找出池化从赚到亏的那条线。 一、池化把什么换成了什么new 是一条线程私有路径。HotSpot 给每个线程一块 TLAB,分配对象就是把指针往前挪一段,再零初始化。 池化把这条路径改成:从共享队列取一个对象、重置它的状态、用完还回去。取和还都要同步。 graph TD subgraph A["每次 new:线程私有"] A1["线程本地 TLAB"] -->|"指针加法 + 清零"| A2["可用的新对象&...
设计模式——访问者:类型与操作的矩阵
访问者模式的经典卖点是「加操作容易、加类型难」:操作轴加一个类,类型轴改遍所有操作。sealed interface 与模式匹配之后,这笔账要重算:switch 也有穷尽性检查,加类型时编译器同样点名。4 类型 × 4 操作的矩阵两种写法都写一遍,量耗时、量两个扩展方向的真实 diff,再看 JDK 24+ 的 ClassFile API 为什么选了访问者这一侧。 一、问题出在哪访问者的机制是双分派。第一次分派给节点,第二次分派给操作: 12345678910111213141516// 节点侧public record Add(Expr left, Expr right) implements Expr { @Override public <R> R accept(ExprVisitor<R> visitor) { return visitor.visitAdd(this); }}// 操作侧public final class EvalVisitor implements E...
Java——JFR 实战:录制、事件与开销实测
JFR 是 JDK 自带的记录器:常态开着也不贵,出事时能把现场和过去几分钟的事件一起拿走。这篇只讲怎么用:录制从哪里发起、录完拿什么读、五百多个事件里先看哪几个。jfr 命令行工具不能发起录制,它只处理录制文件;纯 CPU 负载上 default 与 profile 的开销落在测量噪声里(三档吞吐都是 131k ops/s 上下);分配型负载上,配置之间的差异(0.4%3%)比测量系统自己的轮间漂移(2%4%)还小。数据来自本机(Apple M1 Pro / JDK 25,对照版本 JDK 21.0.8),原始输出贴在每段下面,失效的地方直接写出来。 实验环境与口径12345678910111213$ sysctl -n machdep.cpu.brand_string hw.ncpu hw.memsize kern.osversionApple M1 Pro101717986918425G83$ java -versionjava version "25" 2025-09-16 LTSJava(TM) SE Runtime Envi...
设计模式——迭代器:分配与逃逸分析
ArrayList.iterator() 只有一行实现:return new Itr();。按字面读,每轮 for-each 都新建一个对象。实测 1250 万轮遍历:for-each 的分配是 0 字节,把迭代器存进字段的是 400 MB。差值来自 C2 的逃逸分析。把同一段代码丢给 C1,0 字节这一列就没有了。分配消除是当前那个编译版本的性质,同一份代码换个层级,这一列就变。 一、一行 new 和 0 字节的分配ArrayList 的 iterator() 在 JDK 25 里长这样,java.base/java/util/ArrayList.java:1029: 123public Iterator<E> iterator() { return new Itr();} Itr 定义在同一个文件 :1036,三个 int 字段加一个构造函数: 1234567private class Itr implements Iterator<E> { int cursor; // index of n...
设计模式——模板方法:钩子的成本
模板方法把流程写死在父类,把可变的步骤留给子类。子类通常只需要实现一个方法。其余步骤是带默认实现的钩子,默认体在源码里经常是空的,读起来不要钱。本文量两件事:8 个空钩子在 1 亿次调用下的开销,以及 InputStream 的默认实现把一个批量读请求拆成逐字节调用之后,48 MiB 数据要多付多少。 一、骨架与钩子模板方法把一段流程拆成两种方法:父类实现的骨架方法,子类实现的步骤方法。步骤方法里,一部分是抽象方法,子类必须实现;另一部分是带默认实现的钩子,子类想改才改。 classDiagram direction TB class Template { +run() final #step()* #hook1() #hook2() #hook3() } class ImplA class ImplB Template <|-- ImplA Template <|-- ImplB note for Templa...






