知其然,知其所以然
Java 内部原理 2026-10-05

测试并发资源泄漏

实测并发下的资源泄漏测试:异常路径没关的流,3 次调用测不出,2000 次并发 fd +285;默认 GC 下 System.gc() 把泄漏洗成 0,Epsilon GC 下仍是 +285;每次请求新建线程池 200 次 +400 线程;shutdown 后不等待会瞬时多出 400 线程;容量 10 的池泄漏后之后的调用全部超时。

Java 资源泄漏 文件描述符 线程泄漏 连接池 并发测试 真实验证 动画演示
Java 内部原理 2026-10-05

Testcontainers:用真实依赖做集成测试

在 Docker Desktop 29.8.2 上实测 Testcontainers:1.20.4 因为请求 API v1.32 被引擎拒绝(HTTP 400)而连不上,-Dapi.version=1.44 或升级到 1.21.4 才行;一个 postgres 容器 start 中位数 719 ms,每个测试方法一个容器比 static 容器慢 9.4 s 对 1.8 s;RYUK_DISABLED 加 kill -9 会泄漏容器;H2 会拒绝 4 / 12 条合法的 PostgreSQL 语句。

Java Testcontainers 集成测试 Docker H2 PostgreSQL JUnit 真实验证 动画演示
Java 内部原理 2026-10-05

JUnit 超时测试:防止阻塞悄悄拖慢构建

实测 JUnit 5.11 的超时:@Timeout 靠中断,忙等的测试照样跑满 3 秒才判失败;SEPARATE_THREAD 放弃线程但线程继续跑且 ThreadLocal 丢失;assertTimeout 不中断(3 秒后才失败);timeout.default 能让卡住的 @BeforeEach 1 秒失败;60 ms 的超时在满载机器上 300 次失败 292–296 次。

Java JUnit 超时 Timeout assertTimeout CI 真实验证 动画演示
Java 内部原理 2026-10-05

测试夹具:构造可读且稳定的测试数据

在 731 天里重放“依赖当前时间的夹具”:30 天前的夹具在 300 天失败,昨天的夹具在 208 天(所有周末)失败;随机夹具只在 4.1% 的运行里暴露 bug,记录种子后可 10/10 重放;改一个默认值,断言总价的测试坏 4–5 个而只断言目标项的测试坏 0–1 个;重夹具构造 8.9 微秒。

Java 测试夹具 测试数据 Builder 随机数据 稳定性 真实验证 动画演示
Java 内部原理 2026-10-05

测试数据生成:随机不等于不可复现

实测测试数据生成的可复现性:Random / SplittableRandom / L64X128MixRandom / shuffle 的同种子序列在 JDK 17、21、27 上逐字相同;ThreadLocalRandom 不能设种子;8 线程共享 Random 每次各线程拿到不同的值;并行流里共享 Random 的结果每次不同,按下标派生种子则完全一致;enum 的身份哈希作种子在 JDK 之间不同。

Java 测试数据 随机数 种子 Random SplittableRandom 可复现 真实验证 动画演示
Java 内部原理 2026-10-05

模块边界测试:防止内部 API 越界使用

实测模块边界:未导出的 impl 包在 javac 阶段报 package impl is not visible,同样代码放进 classpath 就没有边界;反射调用要 exports,读私有字段要 opens;--patch-module 的同模块测试能访问 impl 而独立测试模块不能;ArchUnit 在 classpath 上抓到 1 个直接引用,但看不到 Class.forName 的字符串。

Java 模块 JPMS exports opens ArchUnit 模块边界 真实验证 动画演示
Java 内部原理 2026-10-05

发布回滚测试:确认旧版本真的可恢复

在真实 PostgreSQL 16.15 与 Java 序列化上实测回滚:8 种迁移里改名、删列、NOT NULL 无默认值会让 v1 的语句失败;down 迁移让 pending / banned 变成 unknown,DROP COLUMN 后再升级丢掉 3 行 email;序列化的字段在 v1 往返后被静默丢弃;滚动窗口只写新列时 v1 看到 100 行 NULL。

Java 发布 回滚 数据库迁移 序列化 滚动发布 真实验证 动画演示
Java 内部原理 2026-10-05

SynchronousQueue:不存储元素的直接交接

实测 SynchronousQueue:容量 0,offer 无人等待立即 false,put 阻塞时 size / peek / iterator 都看不到元素;newCachedThreadPool 的 50 个阻塞任务得到 50 个线程,ThreadPoolExecutor(2, 4) 在第 5 个任务拒绝;非公平模式是栈([5, 4, 3, 2, 1]);每个对象交接约 2.6 µs,比有缓冲队列的 0.12–0.14 µs 高约 20 倍。

Java 并发 SynchronousQueue 线程池 BlockingQueue 公平性 真实验证 动画演示
Java 内部原理 2026-10-05

LinkedTransferQueue:生产者可以等待消费者

实测 LinkedTransferQueue:tryTransfer 没有消费者就失败且不入队,transfer 会等到该元素自己被取走(前面 3 个元素也要先被取走);慢消费者下 put 1 秒积压 17,978,953 个、transfer 积压 1;size() 是 O(n)(100 万个约 1316 µs);1 对生产消费时比 LinkedBlockingQueue 快,4 对时慢 2 倍多。

Java 并发 LinkedTransferQueue transfer 背压 BlockingQueue 真实验证 动画演示
Java 内部原理 2026-10-05

DelayQueue:到期元素才会变得可取

实测 DelayQueue:poll 只返回已到期元素而 size / isEmpty / peek 包含未到期的;compareTo 与到期时间不一致时 B 被挡在 A 后面晚了约 1900 ms;到期后取到的延迟 p50 约 0.34 ms、p99 约 5–8 ms;4 个消费者只有 1 个取到元素;remove(Object) 随长度线性增长(10 万个约 31788 ns)。

Java 并发 DelayQueue Delayed 定时 BlockingQueue 真实验证 动画演示
Java 内部原理 2026-10-05

PriorityBlockingQueue:优先级队列没有容量上限

实测 PriorityBlockingQueue:12 个相同优先级的任务出队顺序是 [t0, t11, …, t1];toString / stream 是堆数组顺序;入队后修改优先级不会重新排序;持续高优先级到达时低优先级 3 秒内饥饿,按创建时间老化后 2224 ms 被取到;无界队列在 -Xmx64m 放入 62809 个元素后 OOM,Semaphore 限界后稳定在 1000。

Java 并发 PriorityBlockingQueue 优先级队列 饥饿 OOM 真实验证 动画演示
Java 内部原理 2026-10-05

ExecutorService 关闭语义

在 JDK 21 上实测 ExecutorService.close():它等价于 shutdown + 等待终止(909 ms),被中断时改为 shutdownNow;ForkJoinPool.commonPool().close() 什么也不等;ScheduledThreadPoolExecutor.close() 会等延迟任务(1147 ms)、取消周期任务;shutdownNow() 对不响应中断的任务没有效果;JDK 17 无法编译。

Java 并发 ExecutorService close shutdown 线程池 真实验证 动画演示
京ICP备2026067502号-1京公网安备11010502063487号