Java并发编程:全面解析锁策略、CAS与synchronized优化机制
一、六种锁策略场景化解析
1. 乐观锁 vs 悲观锁:图书馆借书的两种策略
核心差异:对资源是否会被抢占的预期不同。
- 乐观锁(假设冲突概率低)
→ 行为:直接去书架上拿书(围绕加锁要做的工作更少)。
→ 风险:可能发现书已被借走(需要重试)。
// 伪代码实现:类似检查版本号
if (当前书未被借) {借书成功;
} else {重新查询;
}
- 悲观锁(假设冲突概率高)
→ 行为:先预定书籍再取书(围绕加锁要做的工作更多)。
→ 保障:确保拿到书时没人争抢。
// 伪代码实现:类似直接加锁
lock(书籍);
借书操作;
unlock(书籍);
2. 轻量级锁 vs 重量级锁:开锁的两种方式
对比维度 | 轻量级锁(密码锁) | 重量级锁(管理员钥匙) |
---|---|---|
开锁速度 | 快(直接输入密码) | 慢(需找管理员申请) |
适用场景 | 短暂使用(如储物柜) | 长期占用(如保险箱) |
资源消耗 | 低(自行操作) | 高(依赖第三方) |
3. 自旋锁 vs 挂起等待锁:等电梯的两种策略
- 自旋锁:属于乐观锁/轻量级锁的一种典型表现。会忙等:等待过程中不会释放cpu资源,一旦锁释放就立即有机会拿到锁。
- 挂起等待锁:属于悲观锁/重量级锁的一种典型表现。不忙等:让出了cpu资源,锁释放后不确定什么时候去拿锁。
4. 公平锁 vs 非公平锁:排队的两种规则
- 公平锁:像银行叫号机,先到先得(需要额外的操作,引入队列时需要记录每个加锁的顺序)。
- 非公平锁:像地铁抢座位,谁快谁得(不需要额外操作,概率均等的让线程去占用锁)。
5. 可重入锁 vs 不可重入锁
核心差异:同一线程能否重复获取同一把锁。
类型 | 行为表现 | 代码示例 | 结果 |
---|---|---|---|
不可重入锁 | 同一线程重复加锁会死锁 | lock(); lock(); | 永久阻塞 |
可重入锁 | 允许同一线程多次加锁 | synchronized void a() { b(); } | 正常执行 |
synchronized void b() {} |
可重入锁三要素:
- 记录持有线程:标记当前锁的归属者。
- 身份验证:新请求的线程需匹配持有者。
- 计时器管理:
- 加锁时
+1
,解锁时-1
。 - 归零时真正释放锁。
- 加锁时
6. 读写锁:图书馆的管理规划
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();// 读者线程
rwLock.readLock().lock(); // 多个读者可同时进入
try {// 读操作
} finally {rwLock.readLock().unlock();
}// 写者线程
rwLock.writeLock().lock(); // 只允许一个写者进入
try {// 写操作
} finally {rwLock.writeLock().unlock();
}
二、synchronized锁升级过程详解
状态流转示意图(该过程不可逆)
各阶段特征
- 偏向锁(占座模式)
- 第一个线程来时做个标记(类似在座位上放本书)。
- 没有真实加锁开销,延迟真正的锁操作。
- 轻量级锁(智能转圈)
- 自适应自旋:JVM动态判断转圈次数(类似智能交通信号灯)。
- 若最近频繁抢到锁 → 允许多转几圈。
- 若多次失败 → 快速放弃转圈。
- 高效场景:适合抢锁时间 < 线程切换时间(约1μs)。
- 重量级锁(系统调度)
- 管理员介入:操作系统维护阻塞队列。
- 释放CPU:线程挂起不消耗CPU资源。
- 恢复延迟:唤醒线程需上下文切换(约10-20μs)。
关键机制图解
三、编译器优化策略
1. 锁消除:去掉多余的锁
public String concat(String s1, String s2) {Object lock = new Object(); // 局部对象不可能被共享synchronized(lock) { // 被编译器优化删除return s1 + s2;}
}
2. 锁粗化:合并相邻的锁
a. 锁粒度定义
锁粒度:指 synchronized
代码块内包含的代码量。
- 细粒度锁:代码量少,加锁范围小(如只包裹一行代码)。
- 粗粒度锁:代码量大,加锁范围广(如包裹整个方法)。
// 细粒度锁示例(不推荐)
public void process() {synchronized(this) { step1(); } // 频繁加解锁// 其他代码...synchronized(this) { step2(); }
}// 粗粒度锁示例(推荐)
public void process() {synchronized(this) { // 合并为单次加锁step1();// 其他代码...step2();}
}
b. 锁粗化(Lock Coarsening)工作原理
优化触发条件:
- 检测到连续相邻的同步块。
- 锁对象相同且无中间非同步代码。
- JIT编译器判定合并后不会显著增加竞争概率。
四、CAS机制详解
1. 核心原理:自动售货机式操作
比喻说明:
CAS操作如同使用自动售货机购买商品:
- 查看标价(读取内存值)
- 投入硬币(准备新值)
- 校验标价(比较内存值)
- 出货取货(原子性更新)
说明:alt
关键字在此处表示条件分支,相当于if
。
2. 硬件级原子性保障
; x86架构实现(CMPXCHG指令)
lock cmpxchg [mem], new_val
; lock前缀锁定总线,保证多核环境原子性
; 比较并交换:若[mem]==EAX寄存器值,则[mem]=new_val
3. CAS具体使用场景
实现原子类
private static AtomicInteger count = new AtomicInteger(0);void increment() {int oldVal, newVal;do {oldVal = count.get(); // ① 读取当前值newVal = oldVal + 1; // ② 计算新值} while (!count.compareAndSet(oldVal, newVal)); // ③ CAS更新
}// 线程安全原理:
// 假设线程A和B同时执行到③:
// - A先执行CAS成功,将0→1
// - B执行CAS时发现当前值≠oldVal(0),循环重试
实现自旋锁
public class SpinLock {private AtomicBoolean locked = new AtomicBoolean(false);// 获取锁public void lock() {while (!locked.compareAndSet(false, true)) { // CAS自旋Thread.yield(); // 让出CPU时间片避免过度消耗}}// 释放锁public void unlock() {locked.set(false);}
}
运行场景分析:
线程行为 | 锁状态变化 | 结果 |
---|---|---|
线程A获取锁 | locked: false → true | 成功,进入临界区 |
线程B尝试获取锁 | 检测到 locked = true | 自旋等待 |
线程A释放锁 | locked: true → false | 线程B CAS成功 |
4. ABA问题深度剖析:重复转账漏洞
场景描述:
张三的银行卡余额为1000元,当他尝试向他人转账500元时,由于网络延迟连续触发了两次转账请求(线程1和线程2)。与此同时,李四向张三账户转入500元(线程3),三个线程的交错执行将导致以下危险操作流:
问题本质:
- 值回退欺骗:CAS机制仅检查当前值是否等于预期值(1000元),无法感知中间发生了
1000→500→1000
的隐形状态变化。 - 资金损失:最终账户余额为500元(正确应为1000-500+500=1000元),张三实际被重复扣款1000元。
版本号解决方案:
通过版本号递增标记数据状态变化,即使值相同也能识别中间修改。
// 账户状态(版本号 + 余额)
AtomicStampedReference<Integer> account = new AtomicStampedReference<>(1000, 0); // 初始值1000元,版本0void transfer(int amount) {int[] stampHolder = new int[1];int oldValue = account.get(stampHolder); // 同时获取值和版本号int newValue = oldValue - amount;// 核心逻辑(适配为Java标准API)if (!account.compareAndSet(oldValue, // 期望值newValue, // 新值stampHolder[0], // 期望版本号stampHolder[0] + 1 // 新版本号(必须递增))) {System.out.println("转账失败:数据已被修改");} else {System.out.println("转账成功");}
}
关键机制拆解:
- 版本号必须递增
stampHolder[0] + 1 // 新版本号必须 > 旧版本号
- 不可逆性:版本号只能增加(类似流水号),确保状态变化的唯一标识。
- 防伪造:阻止恶意或错误的状态回滚(如黑客尝试恢复旧数据)。
- 原子性双重检查
compareAndSet(oldValue, newValue, oldVersion, newVersion)
- 同时校验:值是否变化 + 版本号是否匹配。
- 操作原子性:整个检查-更新过程不可分割。
结果: - 线程1的转账操作因版本号不匹配被拒绝。
- 最终余额正确为1000元(500转出 + 500转入)。
五、总结
本文系统解析Java并发编程核心机制:
- 六大锁策略:涵盖乐观锁与悲观锁、轻量级锁与重量级锁、自旋锁与挂起等待锁、公平锁与非公平锁、可重入锁、读写锁的适用场景及实现原理。
- synchronized优化:通过偏向锁→轻量级锁→重量级锁的升级过程实现性能自适应。
- 编译器优化:锁消除与锁粗化技术减少不必要的同步开销。
- CAS机制:详解原子操作原理、自旋锁实现,以及ABA问题的版本号解决方案。
结语
并发世界如同繁忙的十字路口,锁策略是交通信号灯,CAS是智能感应器,而开发者就是城市交通规划师。只有深刻理解每项机制的设计哲学,才能让数据流如同车流般高效畅通——既不会因过度控制导致拥堵,也不会因管理松散引发事故。记住:最好的并发程序不是最快的那一个,而是在正确性与性能间找到最优平衡的那一个。