引言
在并发编程中,锁机制是保证数据一致性的重要手段。但你是否曾被“悲观锁”和“乐观锁”的概念困扰?它们的区别是什么?各自适用于哪些场景?今天,我将结合自己的实践经验,用通俗的语言和具体示例,带你彻底理解这两种锁的核心理念,并学会如何在项目中正确选择和应用它们。
一、悲观锁:防患于未然的“保守派”
核心思想:假设并发冲突一定会发生,因此提前加锁,阻止其他线程访问资源。
典型实现
- 数据库行锁:例如MySQL的
SELECT ... FOR UPDATE
,在查询时直接锁定数据行。 - Java中的
synchronized
:线程进入同步代码块前必须获得锁。
实战示例
假设有一个电商库存扣减场景:
BEGIN;
SELECT stock FROM products WHERE id=1 FOR UPDATE; -- 加悲观锁
UPDATE products SET stock=stock-1 WHERE id=1;
COMMIT;
适用场景:
- 写操作频繁,冲突概率高
- 临界区执行时间长(如包含IO操作)
缺点:锁的获取和释放带来额外开销,可能引发死锁。
二、乐观锁:相信冲突不常发生的“乐观派”
核心思想:假设并发冲突较少,只在提交时检测是否发生冲突。
典型实现
- 版本号机制:通过
version
字段实现(如MySQL的CAS操作)。 - CAS原子操作:Java中的
AtomicInteger
等工具类。
实战示例
仍以库存扣减为例,使用版本号控制:
UPDATE products
SET stock=stock-1, version=version+1
WHERE id=1 AND version=old_version; -- 若版本号变化则更新失败
或Java中的原子类:
AtomicInteger stock = new AtomicInteger(100);
stock.decrementAndGet(); // 线程安全的无锁操作
适用场景:
- 读多写少,冲突概率低
- 临界区执行快(如纯内存计算)
缺点:冲突频繁时会导致大量重试,反而降低性能。
三、关键对比与选型建议
维度 | 悲观锁 | 乐观锁 |
---|---|---|
冲突假设 | 一定会发生 | 可能不发生 |
实现方式 | 直接加锁 | 版本号/CAS |
性能开销 | 高(锁管理) | 低(无阻塞) |
适用场景 | 短事务、高并发写 | 长事务、低并发写 |
选型原则:
- 冲突频率高 → 悲观锁
- 系统吞吐量优先 → 乐观锁
- 临界区代码执行时间 → 时间长选悲观,短选乐观
四、进阶技巧与避坑指南
-
乐观锁的ABA问题:
使用AtomicStampedReference
替代基础CAS,通过时间戳避免值被篡改后还原导致的误判。 -
混合锁策略:
例如先尝试乐观锁,失败后降级为悲观锁(类似Java的StampedLock
)。 -
数据库隔离级别的影响:
REPEATABLE_READ下乐观锁可能失效,需结合业务场景测试。
结语
理解悲观锁和乐观锁的本质差异后,你会发现没有绝对的“优劣”,只有是否“合适”。建议在实际项目中通过压测验证选择,例如用JMeter模拟并发场景观察锁的表现。
希望这篇文章能帮你摆脱对锁机制的模糊认知。如果有疑问或补充,欢迎在评论区交流——毕竟,技术的进步往往来自思维的碰撞!