Skip to content

乐观锁与悲观锁

悲观锁

一、是什么?

悲观锁是一种并发控制机制,假设多个线程会频繁冲突。在操作数据前,线程会先加锁(如synchronizedReentrantLock),确保操作期间数据独占,其他线程被阻塞。

二、解决什么问题

解决高并发场景下数据一致性问题。例如当多个线程同时修改同一账户余额时,悲观锁强制串行化操作,避免脏读、不可重复读等问题。

三、核心方法

Java实现方式:

  1. synchronized关键字:
    java
    public synchronized void updateBalance() { /* 操作共享数据 */ }
  2. ReentrantLock
    java
    Lock lock = new ReentrantLock();  
    lock.lock();  // 显式加锁  
    try { /* 操作数据 */ } finally { lock.unlock(); }

四、应用场景

  1. 写操作频繁:如银行转账、库存扣减(冲突概率高)。
  2. 强一致性要求:如支付系统,必须保证数据绝对准确。
  3. 数据库中的行锁(如SELECT ... FOR UPDATE)。

五、重要注意事项

  1. 性能开销大:线程阻塞/唤醒消耗资源,可能引发死锁。
  2. 锁粒度需精细:避免锁住整个方法/类,优先用最小范围锁(如代码块而非方法)。

乐观锁

一、是什么?

乐观锁假设操作冲突概率低,线程直接修改数据,提交前通过版本号CAS(Compare and Swap)检测是否被其他线程修改。若冲突则重试或报错。

二、解决什么问题

解决悲观锁的性能瓶颈。通过无锁化设计减少线程阻塞,提高并发吞吐量,适合读多写少的场景。

三、核心方法

  1. CAS操作
    JDK的java.util.concurrent.atomic包提供原子类(如AtomicInteger):
    java
    AtomicInteger balance = new AtomicInteger(100);  
    balance.compareAndSet(100, 90); // 当前值=100时才更新为90
  2. 版本号机制
    数据库或业务层维护版本字段,更新时校验版本:
    sql
    UPDATE account SET balance=90, version=version+1  
    WHERE id=1 AND version=current_version;

四、应用场景

  1. 读多写少:如商品浏览计数、点赞数更新。
  2. 分布式系统:如Redis的WATCH/MULTI命令、ZooKeeper的乐观锁控制。
  3. Java并发工具:StampedLock(JDK8引入,支持乐观读锁)。

五、重要注意事项

  1. ABA问题:数据从A→B→A,CAS误判未修改。
    解决方案:AtomicStampedReference(JDK5+)添加版本戳。
  2. 自旋开销:冲突频繁时重试消耗CPU资源。

乐观锁 vs 悲观锁的区别

维度悲观锁乐观锁
冲突假设假设高冲突,提前加锁假设低冲突,提交时校验
实现方式synchronizedReentrantLockCAS、版本号机制
性能写操作多时性能差(线程阻塞)读操作多时性能高(无阻塞)
数据一致性强一致性最终一致性
适用场景写密集(如支付)读密集(如统计)
典型工具数据库行锁、Java内置锁AtomicXXX类、StampedLock

总结

  1. 悲观锁:适合强一致性+写多读少场景(如金融系统),通过阻塞保证安全,但性能较低。
  2. 乐观锁:适合高并发读+低冲突写场景(如社交应用),通过CAS/版本号提升吞吐量。
  3. JDK8+优化
    • 优先用StampedLock(支持乐观读,避免写饥饿)。
    • 分布式场景结合Redis Lua脚本或ZooKeeper实现乐观锁。
  4. 选择原则
    • 冲突频率高 → 悲观锁;
    • 系统吞吐量优先 → 乐观锁。

示例场景

  • 悲观锁:电商秒杀库存扣减(synchronized锁定库存修改)。
  • 乐观锁:论坛帖子阅读量统计(AtomicLong的CAS更新)。