news 2026/6/11 10:13:04

悲观锁与乐观锁:思想决定命运,到底谁更胜一筹?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
悲观锁与乐观锁:思想决定命运,到底谁更胜一筹?

前言:一场由于“抢票”引发的血案

在多线程和高并发的场景下,最怕的就是共享数据被改乱了。比如两个人同时买最后一张火车票,如果不加控制,两人都以为自己买到了,结果系统扣了两次钱,票却只有一张。为了解决这个问题,我们在程序设计中引入了“锁”的概念。

针对不同的业务场景,大佬们发明了两种截然不同的并发控制思想:悲观锁乐观锁。今天我们就来彻底扒下它们的外衣,看看它们到底是怎么玩的!


一、 悲观锁(Pessimistic Lock)—— “总有刁民想害朕”

1. 核心思想

悲观锁,人如其名,极其悲观。它认为在并发环境下,只要我不把门锁死,别人就一定会来捣乱修改数据
因此,它的策略是:先上锁,再操作。任何想要访问受保护数据的线程,都必须先获取锁。拿不到锁的就在外面排队(挂起/阻塞),直到上一个线程操作完毕释放锁。

2. 典型代表

  • Java中的synchronized关键字。
  • Java中的ReentrantLock
  • 数据库里的表锁、行锁(如SELECT ... FOR UPDATE)。

3. 代码实战与底层解析

我们来看一个经典的转账或者扣库存的例子:

importjava.util.concurrent.locks.ReentrantLock;publicclassPessimisticLockDemo{privateintticketCount=10;// 定义一个典型的悲观锁privatefinalReentrantLocklock=newReentrantLock();publicvoidbuyTicket(StringuserName){// 第一步:一上来就加锁!不给别人任何机会lock.lock();try{// 此时其他线程执行到 lock.lock() 会被全部阻塞(挂起排队)System.out.println(userName+" 准备买票,当前余票:"+ticketCount);if(ticketCount>0){// 模拟业务处理耗时Thread.sleep(100);ticketCount--;System.out.println(userName+" 买票成功!剩余:"+ticketCount);}else{System.out.println(userName+" 买票失败,已经被抢光了...");}}catch(InterruptedExceptione){e.printStackTrace();}finally{// 最后一步:无论是正常结束还是发生异常,必须要释放锁!lock.unlock();}}}

【优缺点剖析】

  • 优点:绝对安全。不管并发多高,数据绝对不会错。
  • 缺点:太重了!线程一直加锁释放锁、阻塞唤醒,需要请求操作系统从用户态切换到内核态,开销极大。这就好比上公共厕所,进去一个人就把大门锁了,外面的人只能干等。

二、 乐观锁(Optimistic Lock)—— “阳光大男孩的自信”

1. 核心思想

乐观锁非常开朗,它认为并发冲突是小概率事件,平时大家各凭本事去拿数据,不会有人来捣乱
因此,它的策略是:不上锁,只在最后更新的时刻去检查一下数据有没有被人动过

  • 如果没被动过,更新成功!
  • 如果被动过了(别人捷足先登),更新失败,然后选择重试(自旋)或者放弃。

2. 核心技术:CAS (Compare And Swap - 比较并交换)

CAS 是乐观锁的灵魂。它包含三个参数:

  • V:内存里的当前值
  • E:我期望的原本旧值
  • N:我想要修改成的新值

执行逻辑:当我要更新时,只有发现内存当前值 V 等于我期望的旧值 E 时,我才把值修改为 N。如果不等于,说明被别人改过了,我就放弃或者重新拿最新的 V 值再试一次。

3. 代码实战与底层解析

Java并发包java.util.concurrent.atomic下的原子类全是乐观锁的实现。

importjava.util.concurrent.atomic.AtomicInteger;publicclassOptimisticLockDemo{// 这是一个利用 CAS 实现的乐观锁整数类,底层无 synchronizedprivateAtomicIntegerticketCount=newAtomicInteger(10);publicvoidbuyTicket(StringuserName){intexpect;// 我期望拿到的旧票数intupdate;// 我抢购完后的新票数// 核心套路:自旋(死循环) + CAS检查do{// 首先看一眼当前的余票是多少expect=ticketCount.get();if(expect<=0){System.out.println(userName+" 发现没票了,放弃挣扎。");return;}// 假设我买走一张,期望新票数update=expect-1;// 下面这行就是 CAS 操作:// "如果当前内存里的票数和我看的时候(expect)还是一致的,那就把它变成 update。"// 如果在这期间有人买走了,ticketCount.compareAndSet 会返回 false,循环继续(这就叫自旋)。}while(!ticketCount.compareAndSet(expect,update));System.out.println(userName+" 买票成功!剩余:"+update+" (使用了乐观理念)");}}

4. 致命漏洞:ABA问题

乐观锁有一个经典 Bug:ABA 问题
老王看一眼桌上的钱是 100 块(这是 A),准备闭眼拿走。这时小偷把 100 块偷走了(变成 B),过了几秒觉得心虚,又放了 100块假钞回去(又变回 A)。老王睁眼一看,还是 100 块(满足CAS条件),开开心心拿走了。但他不知道这中间钱已经被掉包了!

解决方案:加个版本号(Stamp)。每次修改数据不仅对比值,还要看版本号递增了没。Java 中提供了AtomicStampedReference专门用来解决 ABA 问题!


总结:到底谁更胜一筹?

没有绝对的王者,只有最适合的兵器。

  1. 读多写少的场景:冲突较小,使用乐观锁(CAS)。省去了线程挂起的时间,性能极高。
  2. 写多读少的场景:冲突极其激烈。如果还用乐观锁,会导致大量线程在死循环(自旋),把CPU直接打满耗爆!这时候必须用悲观锁(synchronized 或 Lock),乖乖排队才是王道。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/18 22:46:23

STM32主从定时器实战:如何用标准库精准控制PWM波数量(附完整代码)

STM32主从定时器实战&#xff1a;如何用标准库精准控制PWM波数量&#xff08;附完整代码&#xff09; 在嵌入式开发中&#xff0c;精确控制PWM波数量是许多应用场景的核心需求。无论是电机控制、LED调光还是其他需要精确脉冲数量的场合&#xff0c;STM32的主从定时器架构都能提…

作者头像 李华
网站建设 2026/5/18 22:46:25

组态王5.53实战:从零搭建锅炉液位监控系统(附完整工程文件)

组态王5.53实战&#xff1a;从零搭建锅炉液位监控系统&#xff08;附完整工程文件&#xff09; 在工业自动化领域&#xff0c;锅炉液位监控是保障生产安全的重要环节。传统的人工监控方式不仅效率低下&#xff0c;还存在安全隐患。本文将带您使用组态王5.53版本&#xff0c;从零…

作者头像 李华
网站建设 2026/5/18 22:46:22

载人水下快艇技术进展与商业化前景

我们将首先对用户问题进行解构和推演&#xff0c;然后根据现有资料生成详细答案。用户的核心诉求是了解“载人水下快艇”在技术上的研究进展&#xff0c;以及其未来的商业化可能性。 问题解构&#xff1a; 研究对象&#xff1a;载人水下快艇。这是一种结合了水面快艇高速航行…

作者头像 李华
网站建设 2026/5/18 22:46:45

JavaScript实战:用Math对象解决电商页面中的常见计算问题

JavaScript实战&#xff1a;用Math对象解决电商页面中的常见计算问题 在电商开发中&#xff0c;数学计算无处不在——从价格精确到小数点后两位的展示&#xff0c;到限时折扣的倒计时逻辑&#xff0c;再到商品推荐算法的随机性设计。作为前端开发者&#xff0c;我们常常需要处理…

作者头像 李华
网站建设 2026/5/18 22:46:41

1Panel面板快速部署指南:从零开始在Linux服务器上搭建高效运维环境

1. 为什么选择1Panel面板&#xff1f; 如果你正在寻找一款轻量级、易上手的Linux服务器管理面板&#xff0c;1Panel绝对值得考虑。作为一个长期使用各类面板的老运维&#xff0c;我最初是被它的"All in One"理念吸引的试用了两周后&#xff0c;发现它完美解决了传统面…

作者头像 李华