Moshi 的 adapterCache 为什么不使用 ConcurrentHashMap?
结论
ConcurrentHashMap 能保证单个缓存操作的线程安全,但不能直接替代这里的全部同步逻辑。Moshi 还要协调递归创建 adapter、延迟发布缓存,以及合并并发创建的结果。
原问题没有提供 Moshi 版本,两处“here”也没有保留具体代码链接。下面结合可查的官方实现解释其工作机制。这些机制可以说明现有设计的作用,但不能据此断言维护者最初选择容器的全部原因。
两处同步块保护什么
在所核对的实现中,adapterCache 使用 LinkedHashMap。第一处同步块读取缓存,缓存未命中时,在锁外调用 factory 创建 adapter;第二处位于 LookupChain.pop(),在最外层创建成功后提交本次查找产生的缓存项,并处理已有结果。相关逻辑见 Moshi 官方源码。
两个同步块没有锁住整个“查询、创建、写入”过程,也不保证同一个类型只创建一次 adapter。
为什么不能只换成并发 Map
adapter 的创建可能递归。例如,下面的类型会让创建过程再次请求自身的 adapter:
class Node {
Node next;
}
Moshi 通过线程内的 LookupChain 管理这类重入请求,先提供占位 adapter,再连接实际结果。官方源码注释说明,缓存写入要延迟到最外层 adapter 创建成功,以免缓存未完成的占位对象,或依赖这些对象的 adapter。源码说明
替换容器时,需要保留这些发布条件。ConcurrentHashMap 的单次操作安全,不会自动保证多个条目和相关对象状态的整体协调;其批量更新也不向并发读取者提供整体原子性。Java API 文档
尤其不能把现有写入过程中的 put、检查旧值、恢复旧值机械地拆成独立并发操作,否则其他线程可能读到中间值。
能否使用 computeIfAbsent 简化
不能直接套用下面的写法。这里是伪代码,createAdapter 表示实际创建流程:
cache.computeIfAbsent(key, k -> createAdapter(k));
ConcurrentHashMap.computeIfAbsent() 要求计算函数不能在计算期间修改同一个 Map。如果创建流程递归请求其他 adapter,并继续通过该 Map 计算或写入,就需要重新审查这一限制。某些可检测到的递归更新会抛出 IllegalStateException。computeIfAbsent 文档
如果目的是优化性能
可以按以下顺序判断:
- 核对实际使用版本中的缓存读取、
LookupChain和提交逻辑。 - 测量真实负载下的锁竞争,区分缓存命中与首次创建的成本。
- 如果改用
ConcurrentHashMap,同时设计递归处理、成功后发布和并发结果合并,验证递归类型、创建失败及多线程首次请求等场景。
ConcurrentHashMap 可以成为另一种实现的组成部分,但能否提升性能需要测量。仅凭存在两处 synchronized,无法判断当前实现有性能问题,也无法证明替换容器就能改进。
备注:内容仅供参考。
