0%

悲观锁和乐观锁

悲观锁与乐观锁:并发控制的两种核心策略

在多线程环境中,为保证共享资源的一致性,需通过同步机制控制并发访问。悲观锁与乐观锁是两种截然不同的设计思想,分别适用于不同的并发场景。理解它们的原理、适用场景及优缺点,是设计高效并发系统的基础。

悲观锁(Pessimistic Locking)

核心思想

总是假设最坏情况:每次操作共享资源时,都认为其他线程会同时修改该资源,因此必须先加锁,阻止其他线程访问,直到自己操作完成并释放锁。

实现方式

  • 数据库层面:行锁、表锁、读锁(S 锁)、写锁(X 锁)等,如SELECT ... FOR UPDATE会对查询行加排他锁。
  • Java 层面
    • synchronized关键字:隐式加锁,自动释放锁。
    • ReentrantLock:显式加锁(lock())和释放锁(unlock()),支持可重入、中断等特性。

工作流程

  1. 线程访问共享资源前,先尝试获取锁(如synchronized块进入时);
  2. 若获取成功,独占资源并执行操作,期间其他线程会被阻塞(进入BLOCKED状态);
  3. 操作完成后释放锁(如synchronized块退出或unlock()),阻塞线程被唤醒并重新竞争锁。

优缺点

优点 缺点
实现简单,逻辑直观 加锁 / 解锁涉及用户态与内核态切换,开销大
能保证所有操作的原子性 线程阻塞会导致 CPU 利用率降低,吞吐量下降
适用于写操作频繁的场景 可能引发死锁(如锁顺序不一致)

典型应用场景

  • 写操作频繁的场景(如库存扣减、转账交易),避免并发修改导致的数据不一致;
  • 临界区代码执行时间较长的场景(如复杂的业务逻辑),悲观锁可减少无效的重试开销。

乐观锁(Optimistic Locking)

核心思想

总是假设最好情况:每次操作共享资源时,都认为其他线程不会同时修改该资源,因此不加锁直接访问。但在更新资源时,会判断期间是否有其他线程修改过该资源,若未被修改则更新,否则重试或放弃。

实现方式

乐观锁的核心是冲突检测,常见实现方式有两种:

  1. 版本号机制

    • 为资源添加一个版本号(如数据库表的version字段),每次更新时版本号递增;
    • 流程:
      • 读取资源时,记录当前版本号(v);
      • 更新资源时,检查版本号是否仍为v:
        • 若是,更新资源并将版本号改为v+1
        • 若否,说明资源已被修改,放弃更新或重试。

    示例(数据库)

    1
    2
    3
    4
    -- 更新时检查版本号
    UPDATE product
    SET stock = stock - 1, version = version + 1
    WHERE id = 1 AND version = 3; -- 仅当版本号为3时更新
  2. CAS 算法(Compare-And-Swap)

    • 一种无锁原子操作,通过硬件指令保证原子性,核心涉及三个参数:
      • V:共享变量的内存地址;
      • A:预期值(线程读取到的旧值);
      • B:要更新的新值。
    • 逻辑:若内存中V的值等于A,则将V改为B,返回成功;否则返回失败(不修改)。

    示例(Java AtomicInteger)

1
2
3
AtomicInteger count = new AtomicInteger(0);
// 尝试将count从0更新为1(CAS操作)
boolean success = count.compareAndSet(0, 1);

优缺点

优点 缺点
无锁操作,避免线程阻塞 冲突频繁时,重试会消耗大量 CPU 资源
适用于读操作频繁的场景 只能保证单个变量的原子性,无法直接用于代码块
吞吐量高,并发性能好 存在 ABA 问题(可通过版本号解决)

典型应用场景

  • 读操作频繁、写操作较少的场景(如商品详情页浏览、用户信息查询);
  • 低冲突场景(如计数器、序号生成器),乐观锁可减少锁竞争开销。

悲观锁与乐观锁的核心区别

维度 悲观锁 乐观锁
加锁时机 操作前加锁,全程独占资源 操作前不加锁,更新时检测冲突
线程状态 冲突时线程阻塞(BLOCKED) 冲突时线程自旋重试(RUNNABLE)
底层依赖 操作系统的锁机制(如互斥量) 硬件指令(如 CAS)或版本号逻辑
适用场景 写多读少,冲突频繁 读多写少,冲突较少
典型实现 synchronized、ReentrantLock、数据库行锁 Atomic 类、版本号机制、ConcurrentHashMap

解决乐观锁的缺陷

1. ABA 问题

问题:变量值从A变为B再变回A,CAS 会误认为未被修改,导致更新错误。
解决

  • 引入版本号(如AtomicStampedReference),每次修改时版本号递增,更新时同时检查值和版本号;

    1
    2
    3
    4
    5
    // 初始化:值为100,版本号为1
    AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 1);
    int stamp = ref.getStamp(); // 获取当前版本号
    // 仅当值为100且版本号为stamp时,更新为200并递增版本号
    boolean success = ref.compareAndSet(100, 200, stamp, stamp + 1);

2. 自旋开销过大

问题:高并发下 CAS 重试失败次数多,导致 CPU 空转。
解决

  • 限制重试次数(如重试 3 次后放弃);
  • 结合自适应自旋(如 JVM 的轻量级锁,根据历史重试情况动态调整自旋次数);
  • 高冲突场景下改用悲观锁(如LongAdder在高并发时自动切换策略)。

3. 多变量原子性

问题:CAS 仅支持单个变量的原子操作,无法保证多个变量的原子性。
解决

  • 使用AtomicReference封装多个变量为对象,通过 CAS 更新对象引用;
  • 复杂场景下结合锁机制(如先用乐观锁尝试,失败后降级为悲观锁)。

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10

域名更新通知

您好!我们的官方域名已更新为 zhhll.com.cn。 请收藏新域名以获取最佳访问体验。