跳转至

2026-07-24

今日主题

  • 单调时钟 vs 墙上时间
  • Git 三层忽略机制的分工
  • Guava 契约校验设计
  • LocalCache 分段锁并发控制
  • Guava GC 后资源清理
  • Guava API 设计模式
  • GWT 编译器原理与历史
  • lenientFormat 设计意图

新增认知

单调时钟 vs 墙上时间

  • 单调时钟是计时的唯一可靠来源:墙上时间(System.currentTimeMillis())受 NTP 同步、人工调整、时区切换、闰秒等因素影响,
    可能出现时间倒流,导致测出的间隔为负或失真。单调时钟(System.nanoTime())只增不减,专门用于测量过去了多久,不能回答现在是几点。
    Guava 的 Ticker 接口正是基于此设计,文档明确警告只能测 elapsed time,不能测 wall time。

  • elapsed 的技术翻译:在计时场景中,elapsed 首选译为已用时间(工具/API 文档最常用),其次是经过时间(语义中性)和耗时(口语化,
    适合描述操作开销)。流逝时间是直译但偏文学,技术文档中较少使用。

Git 三层忽略机制的分工

  • 三种忽略的作用域本质不同.gitignore 是团队公约(被追踪、共享),
    .git/info/exclude 是私人便签(不追踪、仅本地 clone 生效),全局 gitignore 是本机默认规则(跨仓库)。
    区分它们的关键不是语法(三者语法完全相同),而是文件本身是否被 git 追踪、作用范围是单人还是团队。

  • exclude 的价值恰恰在于"不被追踪":因为 .git/info/exclude 不进版本库,
    所以它适合放那些"只属于我、不该也不方便污染团队 .gitignore"的内容——个人临时脚本、实验文件、本地调试模块。这不是功能缺陷,而是设计意图:
    给每个开发者一个私有的忽略空间,无需协商、无需提交。

  • 全局 gitignore 的协作陷阱:当你把某类文件加入全局忽略后,同事可能误以为"项目 .gitignore 已经覆盖了",结果没人主动提交它,
    造成协作盲区。边界条件:如果你发现自己开始在多个仓库重复 exclude 同一个文件,那它更适合移到全局 ignore;但移入前必须确认团队共识,
    否则会产生"我以为你忽略了"的误解。

Guava 契约校验设计

  • 核心判据是谁的锅:Preconditions 检查调用方是否遵守了方法契约(参数合法、状态合法),失败意味着调用方写错了代码,
    抛 IllegalArgumentException 等标准 JDK 异常。Verify 检查运行时条件是否满足(如远程服务返回意外数据),失败不是调用方的错,
    抛专用的 VerifyException。选型时先问自己:如果这个检查失败,该怪谁?怪调用方用 Preconditions,怪外部环境用 Verify。

  • 三层断言体系:Guava 把契约校验分成递进的三层:① Preconditions——调用方违约,总是检查;② Verify——运行时条件可能失败,
    总是检查;③ Java 原生 assert——绝对不可能发生的条件,默认不检查(需 -ea 参数,本质是编译过的注释)。选择顺序:
    先看是不是调用方的错→是就用 Preconditions;不是就看有没有可能发生→可能就用 Verify;绝不可能→用 assert。

  • VerifyException 的语义信号
    Verify 所有方法统一抛 VerifyException(专用 RuntimeException 子类),
    而不是 IllegalArgumentException/IllegalStateException 这类标准异常。这个设计刻意制造了语义区分——
    读代码的人看到 VerifyException 就知道这是外部条件出了问题,不是编程错误。异常类型本身就是在传递信息。

  • Verify 是始终启用的 assert:Java 原生 assert 默认关闭(需要 -ea JVM 参数),生产环境通常不开启。
    Guava 提供 Verify 的本质动机就是弥补这个缺陷——给你一个语法上和 assert 一样简洁、但永远生效的校验工具。所以 Javadoc 里明确说:
    只要这个检查在现实中有失败的可能,就不要用 assert,用 Verify。

LocalCache 分段锁并发控制

  • isActive 和 isLoading 是正交维度:isActive 回答"槽位在缓存户籍里吗"(容量记账),
    isLoading 回答"有没有线程在干活"(并发协调)。refresh 场景下两者都为 true,这是区分"首次 load"和"刷新旧值"的关键——
    前者 inactive(新创建槽位),后者 active(槽位一直有人占着)。

  • modCount 是全局一致性指纹:分段锁架构下无法同时锁所有 segment,用 modCount 之和代替全局锁。
    只在跨 segment 的批量操作(isEmpty、containsValue)中验证,单 key 读是 lock-free 不需要。
    不一致时 containsValue 无限重试直到连续两轮 modCount 相同,isEmpty 只验证一次就返回。

  • 迭代器弱一致性的保证机制
    持有 table 引用(扩容不影响)+ 三层嵌套遍历(segment → bucket → chain)确保不遗漏创建时已存在的 entry。
    不保证看到创建后的新 entry,这是分段锁架构的标准弱一致语义。

Guava GC 后资源清理

  • FinalizableReferenceQueue 的清理流程:每个 FRQ 实例启动独立的守护线程轮询 ReferenceQueue,
    GC 回收对象时自动调用 finalizeReferent()。用 PhantomReference 监视 FRQ 自身(frqRef),
    当 FRQ 不再被引用时触发线程退出。

  • Finalizer 防 ClassLoader 泄漏:后台线程若强引用 WebappClassLoader 加载的类,
    会导致 ClassLoader 无法卸载。解决方案:用独立 URLClassLoader 加载 Finalizer,
    用弱引用持有 FinalizableReference.class,检测到弱引用失效就退出线程。跨 ClassLoader 调用必须用反射。

Guava API 设计模式

  • 接口分层隔离语义:invalidate 和 remove 底层相同,但语义不同——Map 的删除操作(返回旧值)vs 缓存失效(不关心旧值)。
    通过接口分层(ConcurrentMap vs Cache)和返回类型差异(V vs void)表达不同的使用意图。

  • CharMatcher 三层 API 递进
    工厂方法创建匹配规则(any/whitespace/is/anyOf)→ 布尔组合规则(and/or/negate)→ 字符串处理(trim/remove/retain/replace/collapse)。
    相比正则,可读性更好且可组合,但只支持 BMP 字符(16 位 char),不支持完整 Unicode 代码点。

GWT 编译器原理与历史

  • GWT 吃源码不吃字节码:GWT 编译器读的是 .java 源文件而非 .class 字节码,
    因此 Guava 等库需要单独提供 GWT 源码包才能参与编译。
    这是因为编译过程需要将 Java 语义(对象模型、类型系统、异常处理)逐层转换并优化为 JavaScript,字节码层面丢失了太多源码结构信息。

  • 编译管线:Java 源码 → IR → 优化 → JS:GWT 编译器走传统编译器路线——前端解析出 AST,转为与目标语言无关的中间表示(IR),
    在 IR 层做激进优化(死代码消除、方法内联、常量折叠、类型紧缩),最后生成混淆后的 JavaScript。其优化力度可超过 javac,
    因为你只用了 ArrayList.add(),其他方法会被完全砍掉。

  • JRE 仿真层决定 API 可用性:GWT 自带一份用 Java 写的 java.lang/java.util 子集(以源码发布),
    编译时一起参与转为 JS。String.format() 未被仿真,因为 Formatter 实现复杂、体积大、Locale 模型与浏览器环境差异大,
    收益不成比例。这也是 Guava 提供 lenientFormat() 的直接原因。

  • 衰落脉络:从解决真问题到问题被时代消解:GWT 2006 年解决的是 JS 语言弱 + 浏览器兼容地狱 + Java 团队不想学前端的痛点。衰落路径:
    ES6 让 JS 语言成熟(class/模块/箭头函数)→ TypeScript 补齐类型安全 → React/Vue 的组件化范式取代 Swing 式 GUI → 编译慢、调试痛苦、生态封闭等工程问题被放大 → Google 自身也在撤退。
    本质是它解决的问题被前端生态的进化消解了,属于典型的\"问题消失型淘汰\"。

lenientFormat 设计意图

  • lenientFormat 的安全优先设计:与 String.format() 不同,lenientFormat 在参数和占位符数量不匹配时不抛异常,
    而是尽力输出。这个设计专门服务于异常消息构造和调试日志——在这些场景下,格式化本身抛 IllegalFormatException 会掩盖真正的错误信息。
    Guava 的 Preconditions 类大量使用它,正是为了\"构造异常时不能再出异常\"。