乐观锁与悲观锁
悲观锁
一、是什么?
悲观锁是一种并发控制机制,假设多个线程会频繁冲突。在操作数据前,线程会先加锁(如synchronized
或ReentrantLock
),确保操作期间数据独占,其他线程被阻塞。
二、解决什么问题
解决高并发场景下数据一致性问题。例如当多个线程同时修改同一账户余额时,悲观锁强制串行化操作,避免脏读、不可重复读等问题。
三、核心方法
Java实现方式:
synchronized
关键字:javapublic synchronized void updateBalance() { /* 操作共享数据 */ }
ReentrantLock
:javaLock lock = new ReentrantLock(); lock.lock(); // 显式加锁 try { /* 操作数据 */ } finally { lock.unlock(); }
四、应用场景
- 写操作频繁:如银行转账、库存扣减(冲突概率高)。
- 强一致性要求:如支付系统,必须保证数据绝对准确。
- 数据库中的行锁(如
SELECT ... FOR UPDATE
)。
五、重要注意事项
- 性能开销大:线程阻塞/唤醒消耗资源,可能引发死锁。
- 锁粒度需精细:避免锁住整个方法/类,优先用最小范围锁(如代码块而非方法)。
乐观锁
一、是什么?
乐观锁假设操作冲突概率低,线程直接修改数据,提交前通过版本号或CAS(Compare and Swap)检测是否被其他线程修改。若冲突则重试或报错。
二、解决什么问题
解决悲观锁的性能瓶颈。通过无锁化设计减少线程阻塞,提高并发吞吐量,适合读多写少的场景。
三、核心方法
- CAS操作:
JDK的java.util.concurrent.atomic
包提供原子类(如AtomicInteger
):javaAtomicInteger balance = new AtomicInteger(100); balance.compareAndSet(100, 90); // 当前值=100时才更新为90
- 版本号机制:
数据库或业务层维护版本字段,更新时校验版本:sqlUPDATE account SET balance=90, version=version+1 WHERE id=1 AND version=current_version;
四、应用场景
- 读多写少:如商品浏览计数、点赞数更新。
- 分布式系统:如Redis的WATCH/MULTI命令、ZooKeeper的乐观锁控制。
- Java并发工具:
StampedLock
(JDK8引入,支持乐观读锁)。
五、重要注意事项
- ABA问题:数据从A→B→A,CAS误判未修改。
解决方案:AtomicStampedReference
(JDK5+)添加版本戳。 - 自旋开销:冲突频繁时重试消耗CPU资源。
乐观锁 vs 悲观锁的区别
维度 | 悲观锁 | 乐观锁 |
---|---|---|
冲突假设 | 假设高冲突,提前加锁 | 假设低冲突,提交时校验 |
实现方式 | synchronized 、ReentrantLock | CAS、版本号机制 |
性能 | 写操作多时性能差(线程阻塞) | 读操作多时性能高(无阻塞) |
数据一致性 | 强一致性 | 最终一致性 |
适用场景 | 写密集(如支付) | 读密集(如统计) |
典型工具 | 数据库行锁、Java内置锁 | AtomicXXX 类、StampedLock |
总结
- 悲观锁:适合强一致性+写多读少场景(如金融系统),通过阻塞保证安全,但性能较低。
- 乐观锁:适合高并发读+低冲突写场景(如社交应用),通过CAS/版本号提升吞吐量。
- JDK8+优化:
- 优先用
StampedLock
(支持乐观读,避免写饥饿)。 - 分布式场景结合Redis Lua脚本或ZooKeeper实现乐观锁。
- 优先用
- 选择原则:
- 冲突频率高 → 悲观锁;
- 系统吞吐量优先 → 乐观锁。
示例场景:
- 悲观锁:电商秒杀库存扣减(
synchronized
锁定库存修改)。- 乐观锁:论坛帖子阅读量统计(
AtomicLong
的CAS更新)。