Skip to Content
面试题库Locks[面试题] 乐观锁和悲观锁的区别是什么?

[面试题] 乐观锁和悲观锁的区别是什么?

📖 版块 A:知识讲解版 (温故知新)

  • 悲观锁 (Pessimistic Locking)

    1. 核心思想:假定冲突一定会发生。在每次对数据进行操作时,都会先对其加锁,直到操作完成才释放。
    2. 实现方式:Java 中的 synchronized 关键字、ReentrantLock;数据库中的 SELECT ... FOR UPDATE
    3. 适用场景:写操作频繁、竞争激烈、数据一致性要求极高的场景(如金融转账)。
  • 乐观锁 (Optimistic Locking)

    1. 核心思想:假定冲突不会发生。只在数据提交更新时,检查在此期间是否有其他线程修改了该数据。
    2. 实现方式:CAS (Compare And Swap) 机制、版本号机制(Version)、时间戳机制。Java 中的 AtomicInteger 等原子类是典型代表。
    3. 适用场景:读操作频繁、写操作较少、并发量大但冲突概率低的场景。
  • 核心权衡点

    • 悲观锁的开销在于内核态切换和线程阻塞
    • 乐观锁的开销在于CPU 自旋(重试),如果竞争剧烈,会导致 CPU 飙升。

🎤 版块 B:面试实战版 (高手逐字稿)

Q:乐观锁和悲观锁的区别是什么?

🧠 破局思路:这道题的区分度不在于背诵定义,而在于能否说清楚两种锁在底层资源消耗上的本质差异(上下文切换 vs CPU自旋),以及在不同并发压力下如何做架构决策。

💬 高手逐字稿

关于乐观锁和悲观锁,我通常从“处理冲突的策略”和“性能开销”两个维度来理解。

首先说悲观锁,它的核心逻辑是“先取锁再访问”。它认为数据被修改的概率很高,所以为了保证绝对安全,必须排他性地占有资源。在 Java 里,我们常用的 synchronized 或者数据库层面的 for update 都是典型的悲观锁。它的优点是能严格保证一致性,不会出现更新丢失,但缺点是逻辑较重,会导致线程挂起和唤醒,涉及用户态到内核态的频繁切换,如果竞争激烈,吞吐量会明显下降。

再说乐观锁,它的核心逻辑是“冲突检测”。它假设并发冲突很少,所以平时不加锁,只在最后提交数据时,通过比较版本号或者使用 CAS 算法来判断数据是否被动过。如果没变就更新,变了就重试或报错。比如 Java 的 Atomic 系列类。它的优点是避免了线程切换的开销,非常适合“读多写少”的互联网场景。

但这里有一个细节需要注意,就是乐观锁并不是在任何场景下都比悲观锁快。

当并发竞争非常激烈时,乐观锁会导致大量的 CAS 失败并进入自旋重试,这会极大地消耗 CPU。在这种“高冲突”的情况下,反而使用悲观锁更合适,因为悲观锁会将多余的线程挂起,让出 CPU 给真正执行任务的线程。

另外,使用乐观锁时我们还需要关注 ABA 问题。比如一个值从 A 变成 B 又变回 A,CAS 检查时会认为它没变过。在对过程敏感的业务场景下,单纯的 CAS 就不行了,我们需要引入版本号(Version)或者使用 Java 提供的 AtomicStampedReference 增加戳记来解决。

所以在实际架构中,我通常会遵循一个原则:如果业务逻辑涉及资金、库存等高一致性要求,首选悲观锁来规避风险;如果是缓存同步、点赞数统计等高 QPS 且允许轻微重试的场景,则优先选择乐观锁。

最后更新于:
⏱️

登录后开始阅读

当前 MVP 版本只开放邮箱密码登录注册。完成登录后即可进入面试题内容。

还没有账号?先去注册