1. 简介

MapMaker 是 Guava 提供的一个构建器类,用于简单粗暴地创建线程安全的 Map

Java 标准库中虽然提供了 WeakHashMap,支持对键(key)使用弱引用,但并没有原生支持对值(value)使用弱引用的 Map 实现。✅

MapMaker 正好弥补了这一短板——它通过简洁的 Builder 模式,支持为 key 和 value 分别配置弱引用,极大简化了高级缓存场景的实现。⚠️

本文将带你掌握 MapMaker 的核心用法,特别是如何利用弱引用构建高效、防内存泄漏的缓存结构。


2. Maven 依赖

首先引入 Guava 依赖,当前最新稳定版已包含 MapMaker(注意:在较新版本中部分 API 已标记为过时,但仍可用):

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>32.1.3-jre</version>
</dependency>

⚠️ 注意:从 Guava 12 开始,MapMaker 替代了旧的 MapMakerInternalMap,并在后续版本中逐步演进。本文基于主流稳定版本讲解。


3. 缓存场景示例

假设我们正在开发一个服务端应用,需要维护两种用户缓存:

  • 会话缓存(session cache):用户登出或超时后应自动失效
  • 用户档案缓存(profile cache):仅当用户更新档案时才失效,生命周期较长

理想情况下,我们希望:

  • User 对象被 GC 回收时,对应的 session 缓存条目自动清除(key 使用弱引用)
  • Profile 对象被 GC 回收时,profile 缓存中的 value 自动释放(value 使用弱引用)

这正是 MapMaker 的典型用武之地。

3.1 数据结构定义

先定义三个简单的 POJO 类:

public class User {
    private long id;
    private String name;

    public User(long id, String name) {
        this.id = id;
        this.name = name;
    }

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}
public class Session {
    private long id;

    public Session(long id) {
        this.id = id;
    }

    public long getId() {
        return id;
    }
}
public class Profile {
    private long id;
    private String type;

    public Profile(long id, String type) {
        this.id = id;
        this.type = type;
    }

    public long getId() {
        return id;
    }

    public String getName() {
        return type;
    }
}

3.2 创建缓存实例

使用 MapMaker 创建线程安全的 ConcurrentMap

ConcurrentMap<User, Session> sessionCache = new MapMaker().makeMap();

✅ 该 Map 不允许 key 或 value 为 null,避免了潜在的 NPE 风险。

同样创建 profile 缓存:

ConcurrentMap<User, Profile> profileCache = new MapMaker().makeMap();

📌 默认情况下,MapMaker 创建的 Map 初始容量为 16。若需调整,可通过 initialCapacity() 设置:

ConcurrentMap<User, Profile> profileCache = new MapMaker()
    .initialCapacity(100)
    .makeMap();

3.3 调整并发级别

MapMaker 默认的并发级别(concurrencyLevel)是 4,意味着最多支持 4 个线程并发写入。

但在高并发场景下(如 session cache),可能需要更高并发支持:

ConcurrentMap<User, Session> sessionCache = new MapMaker()
    .concurrencyLevel(10)
    .makeMap();

📌 这个值会影响内部 segment 分段锁的数量,合理设置可减少线程竞争。一般设置为预期并发写线程数。

3.4 使用弱引用

默认情况下,Map 中的 key 和 value 都是强引用,即使外部不再引用它们,Map 仍会持有,容易导致内存泄漏。❌

✅ 场景一:key 使用弱引用(会话缓存)

User 被 GC 后,希望 session 缓存自动失效:

ConcurrentMap<User, Session> sessionCache = new MapMaker()
    .weakKeys()
    .makeMap();

此时,一旦 User 对象不可达,其对应的 entry 将在下一次读/写操作时被自动清理。

✅ 场景二:value 使用弱引用(档案缓存)

Profile 可能较大,希望在无引用时自动释放:

ConcurrentMap<User, Profile> profileCache = new MapMaker()
    .weakValues()
    .makeMap();

这样,当外部不再持有 Profile 实例时,GC 可回收它,Map 中对应 entry 也会被清理。

⚠️ 注意:size() 方法返回的大小可能不准确,因为它不会立即感知到弱引用条目的回收。建议不要依赖 size() 做精确判断。


4. MapMaker 内部原理

MapMaker 的底层实现会根据配置动态选择:

配置情况 实际生成的 Map 类型
未启用弱引用 ConcurrentHashMap
启用 weakKeys 或 weakValues 自定义的 MapMakerInternalMap

📌 关键差异:

  • 普通情况:使用 equals()hashCode() 判断 key 相等性
  • 启用弱引用后:改用 引用相等性(identity equality),即 ==System.identityHashCode()

这意味着:即使两个对象 equals() 返回 true,只要它们不是同一个实例,就会被视为不同的 key。⚠️

💡 这是为了避免弱引用回收过程中出现状态不一致的问题,属于设计取舍。


5. 总结

MapMaker 虽然在新版本 Guava 中部分 API 已被标记为过时(推荐使用 CacheBuilder 替代),但在一些轻量级、高性能的场景中依然有其价值。

本文重点总结:

✅ 支持 weakKeys()weakValues(),解决标准库无法对 value 使用弱引用的问题
✅ 可设置 initialCapacityconcurrencyLevel,优化性能
✅ 自动生成线程安全的 ConcurrentMap,开箱即用
⚠️ 启用弱引用后使用 identity 比较,需注意语义变化
size() 方法在弱引用场景下可能不准,避免用于精确控制

📌 实际项目中,若涉及复杂缓存策略(如 TTL、 maxSize、自动加载),建议优先考虑 CacheBuilder。但对于简单场景,MapMaker 依然是个不错的“轻骑兵”。

完整示例代码已托管至 GitHub: https://github.com/baomidou/tutorials/tree/master/guava-modules/guava-collections-map


原始标题:Using Guava’s MapMaker

« 上一篇: Java Weekly, 第328期
» 下一篇: Java Number 类详解