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

前言

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

悲观锁:顾名思义,就像是一个悲观的人,总会觉得别人会修改数据,所以在操作数据之前先把数据数据锁住,自己独占使用,其他线程必须等到我释放之后才可以使用,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自增底层

java 复制代码
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();

底层核心源码(JDK8)

java 复制代码
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更新、失败就继续循环(自旋乐观锁)直到成功

java 复制代码
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 实现无锁原子并发。

java 复制代码
private volatile int value;

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

1.实现思路

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

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

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

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

2.SQL示例

(1)查询

sql 复制代码
select id, name, version from user where id = 1;

(2)更新

sql 复制代码
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 加锁、阻塞、切换开销固定
典型场景 读多写少、缓存、计数 写多读少、资金交易、库存扣减
相关推荐
SimonKing1 小时前
OpenCode 在 IDEA 中使用 ACP 协议 VS 直接使用 TUI,哪个编程方式更是你的菜?
java·后端·程序员
NE_STOP2 小时前
Redis--持久化之AOF
java
budingxiaomoli2 小时前
注册中心的其他实现-Nacos
java·spring cloud·微服务
大大大大晴天️2 小时前
Flink技术实践-Flink重启策略选型指南
java·大数据·flink
ffqws_2 小时前
Spring @Transactional 注解详解:从入门到避坑
java·数据库·后端·spring
xuhaoyu_cpp_java2 小时前
单调栈(算法)
java·数据结构·经验分享·笔记·学习·算法
黑夜里的小夜莺2 小时前
黑马点评登录成功后点击【我的】会跳转到登录页面 BUG 修复
java·bug
wuyikeer3 小时前
Java进阶——IO 流
java·开发语言·python
fengxin_rou3 小时前
JVM 内存结构与内存溢出 / 泄漏问题全解析
java·开发语言·jvm·分布式·rabbitmq