当前位置: 首页 > news >正文

深入剖析乐观锁背后的原理

前言

在并发编程中,解决共享资源线程安全主要有两大思想:悲观锁、乐观锁

悲观锁:顾名思义,就像是一个悲观的人,总会觉得别人会修改数据,所以在操作数据之前先把数据数据锁住,自己独占使用,其他线程必须等到我释放之后才可以使用,Java中的synchronized就是悲观锁的体现(之前有文章详细的介绍),虽然会保证线程安全,但是会带来线程阻塞、上下文切换、并发吞吐量低等问题

乐观锁:就是一个乐观的人,觉得别人不会同时修改数据,所以不对数据加锁,直接操作数据,更新时在检查:有没有人动过数据?若没有人动,就更新成功;如果有人动,就放弃本次修改或重试,相比于悲观锁,更适合高并发场景

一、乐观锁核心思想

1.定义

乐观锁是一种无锁并发控制思想:默认认为多线程之间很少发生数据竞争不上锁、不阻塞,直接读写共享数据;在提交更新的那一刻,校验数据是否被其他线程修改过:

  • 未被修改:更新成功;
  • 已被修改:放弃本次更新,或自旋重试。

2.核心特征

  • 不加独占锁:无线程阻塞,无队列等待;
  • 事后校验:读的时候不校验,更新时才校验
  • 冲突自愈:发现竞争冲突,不阻塞,通过自旋重试解决;
  • 最终一致性:不保证瞬时强一致,保证最终数据一致。

3.与悲观锁区别

悲观锁:先加锁→再操作,悲观预判必有竞争,独占资源;

乐观锁:先操作→后校验,乐观预判少有竞争,无锁并行。

更详细的对比见文章末尾

二、乐观锁两大主流实现方式

1.CAS无锁实现(Java内存并发常用)

全称:Compare And Swap,比较并交换

是 CPU 硬件级支持的原子指令,也是 Java 中乐观锁的底层基石。详细看第三点

2.版本号机制(数据库分布式并发常用)

数据表增加version版本字段,更新时携带旧版本号匹配,不匹配则更新失败。详细看第五点

三、CAS算法(Java内存并发常用)

1.定义

是CPU 硬件级支持的原子指令,也是 Java 中乐观锁的底层基石。思想就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新

2.三个核心参数

CAS(V, E, N):

  • V (Variable):内存中共享变量的实际值
  • E (Expected):线程预期原值(自己读取到的值)
  • N (New):想要修改的新值

3.执行逻辑(原子不可分割)

当且仅当V的值等于E时,CAS通过原子方式用新值N来更新V的值,如果不等,说明已经有其他线程更新了V,则当前线程放弃更新

举一个简单的例子:线程A要改变变量 i 的值为6,i 原值为1(V=1,E=1,N=6):i与1进行比较,如果相等,则说明没有被其他线程修改,可以设置为6;如果不相等,则说明被其他线程修改,当前线程放弃更新

当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余都会失败

4.CAS为什么能实现乐观锁

  • 全程没有加锁、没有阻塞
  • 依靠读取原值 → 运算 → CAS 校验更新三步;
  • 失败后不阻塞,循环重试(自旋),直到更新成功。

5.CPU底层支撑

CAS 不是操作系统实现,也不是 Java 语言实现,依赖 CPU 总线锁 / 缓存锁:

  • 多核 CPU 下,通过缓存行锁定保证 CAS 指令原子性;
  • 避免多线程同时修改同一缓存行数据,从硬件层面保障操作不可分割。

四、Java中CAS源码剖析

JUC 包下原子类全部基于 CAS 实现乐观锁:AtomicIntegerAtomicLongAtomicBooleanAtomicReference等。

1.Atomiclnteger自增底层

AtomicInteger count = new AtomicInteger(0); count.incrementAndGet();

底层核心源码(JDK8)

public final int incrementAndGet() { return getAndAdd(1) + 1; } public final int getAndAdd(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta); }

里面涉及到的:

  • Unsafe 类:Java 底层不安全类,可以直接操作内存、调用 CPU 原子指令;
  • valueOffset:变量在内存中的偏移地址,直接定位内存数据。

Unsafe本地方法自选逻辑:

核心流程:循环读取内存最新值、尝试CAS更新、失败就继续循环(自旋乐观锁)直到成功

public final int getAndAddInt(Object o, long offset, int delta) { int v; // 自旋循环:CAS失败就一直重试 do { // 从内存中读取当前最新值 v = getIntVolatile(o, offset); // CAS比较并交换:内存值是否等于v,是则+delta,否则循环重试 while (!compareAndSwapInt(o, offset, v, v + delta)); return v; }

2.volatile配合CAS的作用

原子类中变量都被volatile修饰:

volatile 保证可见性:线程每次都从主内存读最新值,不从工作内存缓存读;

volatile 不保证原子性:所以必须靠 CAS 补全原子操作;组合volatile + CAS实现无锁原子并发。

private volatile int value;

五、版本号机制(数据库场景)

1.实现思路

(1)数据库表新增字段version int default 1

(2)查询数据时,同时查出当前version

(3)更新数据时,必须携带旧版本号作为条件;

(4)更新成功则版本号 + 1,版本不匹配则更新失败。

2.SQL示例

(1)查询

select id, name, version from user where id = 1;

(2)更新

update user set name = '新名称', version = version + 1 where id = 1 and version = 旧版本号;

3.执行逻辑

  • 线程 A 查到 version=1;
  • 线程 B 同时查到 version=1,并抢先更新为 version=2;
  • 线程 A 再用 version=1 去更新,条件不匹配,更新行数为 0,失败;
  • 业务层可选择:抛出异常、重试、放弃操作。

六、CAS乐观锁三大致命问题

1.ABA问题

(1)现象

线程 1 读取值 A,准备 CAS 更新;线程 2 先把 A 改成 B,又改回 A;线程 1 CAS 发现还是 A,认为没被修改,正常更新,忽略了中间被篡改的过程

(2)危害

会导致数据状态被覆盖,业务逻辑出错(如资金、库存)。

(3)解决方案

  • 增加版本号:每次修改版本自增,即使值变回 A,版本也不同;

  • JDK 工具类AtomicStampedReferenceAtomicMarkableReference携带版本戳 / 标记位,不仅比较值,还比较版本。

2.循环自旋消耗CPU

(1)现象

CAS 冲突严重时,会无限循环重试,一直占用 CPU,导致 CPU 飙高。

(2)解决

  • 限制自旋次数,超过次数改用悲观锁;
  • 自适应自旋(JDK 锁优化思想)。

3.只能保证单个变量原子性

(1)现象

CAS 只能对一个变量做原子更新;如果需要同时修改多个共享变量,CAS 无法保证原子性。

(2)解决

  • 使用AtomicReference封装对象,把多个变量装进一个对象;
  • 改用悲观锁ReentrantLock/synchronized

七、乐观锁 VS 悲观锁

对比层面乐观锁悲观锁
加锁思想无锁,事后校验独占锁,事前加锁
底层实现CAS、版本号synchronized、ReentrantLock
线程状态自旋重试,不阻塞阻塞挂起,让出 CPU
并发性能高,无阻塞开销低,有上下文切换
一致性最终一致强一致性
开销竞争小开销低,竞争大耗 CPU加锁、阻塞、切换开销固定
典型场景读多写少、缓存、计数写多读少、资金交易、库存扣减
http://www.jsqmd.com/news/759332/

相关文章:

  • DROID-SLAM的“可微分BA层”到底强在哪?深入拆解RAFT与LieTorch的协同设计
  • 从Kaggle竞赛到真实业务:我是如何用SHAP值说服医生信任我的‘患者再入院风险’模型的
  • 新手零门槛入门:在快马平台完成你的第一个hermes-agent安装与测试
  • STM32 PID温控终极指南:从零到精通的5个实战技巧
  • AI智能体技能开发实战:从LLM工具封装到复杂任务自动化
  • 别再手动写CRUD了!用avue-crud快速搞定Vue后台表格(附ElementUI配置避坑)
  • 3步掌握Layerdivider:智能图像分层的高效解决方案
  • 观察 Taotoken 按 Token 计费模式下的成本控制效果
  • 5步实施指南:开源SENAITE LIMS如何重塑实验室数字化转型路径
  • 无人机姿态控制实战:用Python从零搭建四元数PD控制器(附完整仿真代码)
  • 别再傻傻分不清了!一文讲透Autosar CP和AP到底该怎么选(附MCU/MPU芯片清单)
  • 终极指南:如何用WorkshopDL轻松下载Steam创意工坊模组
  • :简单 RAG 入门
  • Nacos 2.2.3安装后登录失败?手把手教你排查鉴权密钥与数据库配置问题
  • TrollInstallerX终极安装指南:iOS越狱工具快速安装与故障排除
  • SeeUPO算法:无Critic强化学习在序列决策中的应用
  • 告别‘一病一药’:用PromptIR这个‘万能插件’搞定所有图像修复难题(含代码实战)
  • 别再只用SSH了!给CentOS 7/8装个图形桌面,用Windows远程桌面直接连(xrdp保姆级教程)
  • 从亚马逊招聘工具到Midjourney翻车:给产品经理的AI偏见风险自查清单
  • Proteus仿真实战:用51单片机驱动6位数码管显示温度计(附完整C代码)
  • Linux深色光标主题设计、安装与自定义全指南
  • LLM代理在科研智能化中的实践与架构设计
  • Multisim 14.2 实战:用运放和RLC电路,手把手教你从零搭建一个五级DAC
  • PyInstaller打包的Matplotlib程序从40MB瘦身到17MB:我的实战记录与思考
  • Pearcleaner:免费开源的Mac应用清理工具,彻底释放存储空间
  • 用Python爬取中国福利彩票官网数据,自动更新到Excel的完整代码(附避坑指南)
  • 从图像分类到CTR预估:手把手拆解SENET模块在FiBiNet中的迁移与应用
  • 终端字符串样式化:从ANSI原理到Chalk库的实战指南
  • 三分钟掌握Steam Depot清单下载:Onekey工具终极指南
  • 从LC谐振到相位噪声:手把手教你理解VCO核心原理与设计权衡