经典的Java双重检查锁代码
文章目录
- 前言
- 📐 一、整体逻辑
- ⏰ 二、为什么提前 5 分钟?
- 🔒 三、双重检查锁详解
- 3.1 为什么要用双重检查?
- 3.2 执行流程图解
- 💡 四、`volatile` 关键字的作用(关键!)
- 4.1 volatile 的两个作用
- 4.2 为什么必须加 volatile?
- 🎯 五、为什么这样设计?
- 5.1 三种方案对比
- 5.2 配合定时任务
- 📊 六、总结这段代码的知识点
- 🎓 七、学习建议
前言
经典的双重检查锁代码!这是 Java 并发编程的绝佳学习案例!
📐 一、整体逻辑
publicStringgetAccessToken(){// 步骤 1:第一次检查(不加锁)if(System.currentTimeMillis()>expireTime-5*60*1000){// 步骤 2:加锁synchronized(this){// 步骤 3:第二次检查(加锁后再次检查)if(System.currentTimeMillis()>expireTime-5*60*1000){// 步骤 4:真正刷新 tokenrefreshToken();}}}returnaccessToken;}⏰ 二、为什么提前 5 分钟?
| 原因 | 说明 |
|---|---|
| 缓冲时间 | 避免在 token 即将过期的临界点出问题 |
| 网络延迟 | 刷新 token 可能需要网络请求,提前刷新确保安全 |
| 双重保险 | 定时任务是 110 分钟(提前 10 分钟),代码里又提前 5 分钟,双重保险 |
🔒 三、双重检查锁详解
3.1 为什么要用双重检查?
| 问题 | 说明 |
|---|---|
| 性能问题 | 如果每次都加锁,高并发时会成为瓶颈 |
| 线程安全 | 不加锁可能导致多个线程同时刷新 |
3.2 执行流程图解
假设有 3 个线程同时调用:
时间线: ├─ 线程 A 进入,第一次检查:需要刷新 ├─ 线程 A 获取锁 ├─ 线程 B 进入,第一次检查:需要刷新 ├─ 线程 B 等待锁(被阻塞) ├─ 线程 A 第二次检查:需要刷新 → 刷新 token ├─ 线程 A 释放锁 ├─ 线程 B 获取锁 ├─ 线程 B 第二次检查:token 已刷新!不刷新! ✅💡 四、volatile关键字的作用(关键!)
看第 35、38 行:
privatevolatileStringaccessToken;// ⚠️ 注意 volatile!privatevolatilelongexpireTime=0;4.1 volatile 的两个作用
| 作用 | 说明 |
|---|---|
| 1. 可见性 | 一个线程修改,其他线程立刻看到 |
| 2. 禁止指令重排序 | 防止 CPU 对指令重新排序 |
4.2 为什么必须加 volatile?
如果没有 volatile,会有什么问题?
// 伪代码:refreshToken 里的操作this.accessToken=newToken;// 步骤 1this.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分钟提前过期 |
🎓 七、学习建议
- 先理解单线程版本:不考虑并发,先写出简单版本
- 再理解同步版本:每次都加锁,保证线程安全
- 最后理解双重检查锁:在同步版本基础上优化性能
- 学习 volatile:看《Java 并发编程实战》第 3 章
