前言

经典的双重检查锁代码!这是 Java 并发编程的绝佳学习案例!


📐 一、整体逻辑

public String getAccessToken() {
    // 步骤 1:第一次检查(不加锁)
    if (System.currentTimeMillis() > expireTime - 5 * 60 * 1000) {
        // 步骤 2:加锁
        synchronized (this) {
            // 步骤 3:第二次检查(加锁后再次检查)
            if (System.currentTimeMillis() > expireTime - 5 * 60 * 1000) {
                // 步骤 4:真正刷新 token
                refreshToken();
            }
        }
    }
    return accessToken;
}

⏰ 二、为什么提前 5 分钟?

原因 说明
缓冲时间 避免在 token 即将过期的临界点出问题
网络延迟 刷新 token 可能需要网络请求,提前刷新确保安全
双重保险 定时任务是 110 分钟(提前 10 分钟),代码里又提前 5 分钟,双重保险

🔒 三、双重检查锁详解

3.1 为什么要用双重检查?

问题 说明
性能问题 如果每次都加锁,高并发时会成为瓶颈
线程安全 不加锁可能导致多个线程同时刷新

3.2 执行流程图解

假设有 3 个线程同时调用:

时间线:
├─ 线程 A 进入,第一次检查:需要刷新
├─ 线程 A 获取锁
├─ 线程 B 进入,第一次检查:需要刷新
├─ 线程 B 等待锁(被阻塞)
├─ 线程 A 第二次检查:需要刷新 → 刷新 token
├─ 线程 A 释放锁
├─ 线程 B 获取锁
├─ 线程 B 第二次检查:token 已刷新!不刷新! ✅

💡 四、volatile 关键字的作用(关键!)

看第 35、38 行:

private volatile String accessToken;  // ⚠️ 注意 volatile!
private volatile long expireTime = 0;

4.1 volatile 的两个作用

作用 说明
1. 可见性 一个线程修改,其他线程立刻看到
2. 禁止指令重排序 防止 CPU 对指令重新排序

4.2 为什么必须加 volatile?

如果没有 volatile,会有什么问题?

// 伪代码:refreshToken 里的操作
this.accessToken = newToken;        // 步骤 1
this.expireTime = newExpireTime;    // 步骤 2

没有 volatile 时,CPU 可能会重排序:

this.expireTime = newExpireTime;    // 步骤 2(先执行)
this.accessToken = newToken;        // 步骤 1(后执行)

这就导致:

  • 线程 A 刚把 expireTime 设置为新值,但 accessToken 还没更新
  • 线程 B 看到 expireTime 是新的,就直接返回 accessToken,但 accessToken 还是旧的!❌

🎯 五、为什么这样设计?

5.1 三种方案对比

方案 优点 缺点
每次都刷新 简单 性能差,微信 API 调用频繁
每次都加锁 线程安全 性能差,高并发时瓶颈
✅ 双重检查锁 性能好 + 线程安全 实现稍复杂

5.2 配合定时任务

// 定时任务:每 110 分钟刷新一次
scheduler.scheduleAtFixedRate(this::refreshToken, 110, 110, TimeUnit.MINUTES);

定时任务 + 双重检查 = 双保险!

  • 正常情况下:定时任务自动刷新
  • 异常情况下:getAccessToken() 主动刷新

📊 六、总结这段代码的知识点

知识点 位置 说明
volatile 第 35、38 行 保证可见性,禁止指令重排
双重检查锁 第 79-84 行 性能与线程安全的平衡
synchronized 第 80 行 保证原子性
时间计算 第 79 行 expireTime - 5分钟 提前过期

🎓 七、学习建议

  1. 先理解单线程版本:不考虑并发,先写出简单版本
  2. 再理解同步版本:每次都加锁,保证线程安全
  3. 最后理解双重检查锁:在同步版本基础上优化性能
  4. 学习 volatile:看《Java 并发编程实战》第 3 章

更多推荐