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

HashMap 树化:真实测出是插入第 9 个才变红黑树,不是第 8 个

用一个 hashCode 永远返回 42 的坏类反射进 HashMap 的 table:真实测出桶内已有 8 个节点、插入第 9 个才真正树化;起始容量不到 64 时即使节点数早过 8,也只会连续扩容(真实测到 16 到 32 到 64),不会树化;remove 一路删到 0 个,类型全程真实保持 TreeNode,没有按常见说法在剩 6 个时转回链表——那是 resize 拆分时的逻辑,不是 remove 的逻辑。

Java HashMap 红黑树 动画演示
Java 内部原理 2026-05-14

ThreadLocal 内存泄漏:线程池里不调用 remove(),真实测出 23MB 残留

真实踩坑:第一次实验线程跑完就退出,反射进去发现 threadLocals 字段直接是 null——因为 JVM 在线程真实退出时会自动清空这个字段,得让线程模拟线程池那样跑完活之后卡住待命,才测得到真实的残留;改用这个方式后,500 次创建 100KB 的值、不调用 remove(),真实反射进 Thread 的 ThreadLocalMap,数出 238 个 key 已被弱引用回收、但 value 依然真实占着内存的残留条目,合计真实吃掉约 23MB;每次用完调用 remove() 之后,残留条目和字节数都真实变成 0。

Java 并发 ThreadLocal 动画演示
Java 内部原理 2026-05-13

ReentrantLock:公平锁和非公平锁,真实差了 58 倍耗时

真实起 4 个线程各抢锁 2 万次,统计同一个线程连续拿到锁的最长次数——非公平锁真实测出最长连续 2346 次(同一个线程反复插队),公平锁只有 134 次;耗时上非公平锁真实只用 7ms,公平锁真实用了 411ms,慢了 58 倍;两次独立运行结果趋势一致,解释清楚公平锁为什么慢——每次都要老老实实唤醒排在队首的线程,而不是让刚释放锁的线程直接再抢一次。

Java 并发 ReentrantLock 动画演示
Java 内部原理 2026-05-12

ConcurrentHashMap:多线程 put 为什么不会锁住整张表

真实起 16 个线程并发 put 80 万个不重复的 key 进普通 HashMap,最终 size() 真实只剩 656061,丢了将近 18%,没有抛任何异常;换成 ConcurrentHashMap 同样的并发写入,size() 真实精确等于 800000;真实验证锁粒度——一个线程在 keyA 上执行耗时 800ms 的 compute,另一个线程同时 put 完全不相关的 keyB,用 synchronizedMap 包装的 HashMap 真实被阻塞等了 697ms,换成 ConcurrentHashMap 真实 0 毫秒立刻返回,证明它锁的是单个桶而不是整张表。

Java 并发 ConcurrentHashMap 动画演示
Java 内部原理 2026-05-11

ArrayList 扩容:5,000,000 次 add,不预设容量真实多搬运了 1230 多万个元素

真实反射读出 ArrayList 的 elementData 字段,证明刚 new 出来的空 ArrayList 共享同一个哨兵数组,第一次 add 才真实分配容量 10;真实扫描 200 次 add,验证扩容公式就是旧容量 + 旧容量右移一位(约 1.5 倍),10→15→22→33→49→73→109→163→244 逐个对上;真实添加 500 万个元素,不预设容量的版本底层数组真实重新分配了 34 次、累计搬运 12306801 个元素引用,预设容量的版本重新分配 0 次、搬运 0 个——用真实数字量化了能预估数据量就指定初始容量这条建议到底值多少。

Java ArrayList 扩容 动画演示
Java 内部原理 2026-05-10

volatile:为什么能保证可见性却不能保证 i++ 安全

真实起 8 个线程各累加 10 万次,plain int 和 volatile int 两种写法真实都把结果丢到只剩 15%~23%(期望 80 万,真实测出 111838~180720 不等,每次运行结果都不一样),只有 AtomicInteger 真实稳定跑出 800000;真实复现可见性问题——一个不带 volatile 的停止标志位,工作线程真实卡住超过 3 秒收不到停止信号,换成 volatile 后 0 毫秒立刻退出,两组数据对照着讲清楚 volatile 保证的到底是可见性还是原子性。

Java 并发 volatile 动画演示
Java 内部原理 2026-05-09

Integer 缓存:为什么 == 在测试环境正常,生产环境却悄悄失灵

真实用反射读出 Integer 私有缓存类 IntegerCache 的 low/high 字段和 cache 数组,证明 -128~127 范围内的自动装箱确实复用同一个对象;真实扫描找到 == 从 true 翻转成 false 的精确边界正好是 128;真实复现一个把 == 当 equals 用的计数器 bug——用户 ID 小于 128 时测试环境一切正常,真实数据里 ID 一旦超过 128,重复访问计数真实变成了 0。

Java Integer 自动装箱 动画演示
Java 内部原理 2026-05-08

String 内部:coder 决定 1 字节还是 2 字节,「不可变」只是约定——反射改一个字节,字面量、HashMap 全被改了

关于 String 有两个流传很广的说法:JDK 9 之后引入了 Compact Strings,纯拉丁字符的字符串用 1 个字节存一个字符,含其他字符才用 2 个字节;String 是不可变的。这次用本机的 OpenJDK 17 逐个验证:用反射读出 String 内部的 value(byte)

Java String JVM 动画演示
Java 内部原理 2026-05-07

HashMap 扩容全过程:第 13 个 put 才触发,节点按 hash 的一位拆成两桶,预设容量省掉 18 次扩容

HashMap 的扩容有几个流传很广的说法:默认容量 16、负载因子 0.75、放到第 13 个元素扩容、扩容后节点"重新计算位置"。这次不背,全部在本机的 OpenJDK 17 里用反射读 HashMap 内部的 table 和 threshold 字段,逐个 put 地看:table 什么时候

Java HashMap JVM 动画演示
算法与数据结构 2026-01-26

K 个一组翻转链表:递归版真实爆栈,迭代版怎么写

真实实现 K 个一组翻转链表:递归版结果和 8 组边界用例(整除、有余数、k=1、k 等于链表长度、k 超过链表长度、空链表)跟暴力法交叉验证全部一致;真实踩坑——20 万节点、k=7 时递归版真实触发 Python RecursionError 爆栈;改成迭代版后同样数据 15.9 毫秒跑完,和递归版结果逐一交叉验证一致。

链表 递归 动画演示
算法与数据结构 2026-01-25

合并区间:排序贪心是怎么处理嵌套和重叠的

真实验证合并区间的排序贪心算法:嵌套区间、部分重叠、共享端点三种情况和暴力覆盖标记法结果完全一致;真实踩坑发现“端点相邻但不共享点算不算重叠”这个边界情况,连暴力验证法本身都有盲区,必须先分清连续实数语义和离散整数语义;真实实现差分数组版本作为第二种解法,和排序贪心版交叉验证,值域有限时明显更快。

区间合并 排序 动画演示
算法与数据结构 2026-01-24

差分数组:区间批量加,为什么只改两个位置就够了

真实验证差分数组处理“区间批量加”的正确性:4 次区间加操作,和暴力遍历逐个位置累加的结果完全一致;50 万长度数组、2 万次随机区间加,真实测出差分数组比暴力遍历快 72.9 倍;真实把差分数组接上高峰在线人数的例子,验证结果一致——这正是之前那几篇“线性扫描”算法背后真正在用的数据结构。

差分数组 前缀和 动画演示
京ICP备2026067502号-1京公网安备11010502063487号