一、核心概念与设计思想
1. 悲观锁(Pessimistic Lock)
定义
悲观锁秉持悲观的态度 :认为并发操作一定会发生冲突 ,所以在整个数据处理流程中 ,都会将数据加锁,其他线程 / 事务想要操作该数据时,必须阻塞等待,直到锁被释放。简单理解:先加锁,再操作,独占资源。
核心思想
假设冲突是常态,通过阻塞其他请求来避免并发问题,强一致性优先,性能次之。
2. 乐观锁(Optimistic Lock)
定义
乐观锁秉持乐观的态度 :认为并发冲突极少发生 ,不会在业务执行过程中加锁 ,仅在最终提交数据更新时,通过校验机制判断数据是否被其他线程修改过。
- 如果未被修改:正常提交更新;
- 如果已被修改:拒绝更新,返回失败,由业务层决定重试 / 报错。
简单理解:无锁操作,提交时校验,冲突则回退。
核心思想
假设冲突是特例,通过无锁 + 版本校验提升并发性能,高性能优先,一致性通过重试保证。
二、具体实现方式(Java 后端开发常用)
结合后端开发最常用的数据库 、Java 原生并发工具 、分布式场景,分别讲解两种锁的落地实现。
(一)悲观锁实现方案
方案 1:数据库行锁 / 表锁(SQL 层面)
关系型数据库(MySQL InnoDB)原生支持悲观锁,通过FOR UPDATE语法实现排他锁 ,仅支持事务内生效。
适用场景
单体应用、短事务、强一致性要求的操作(如订单扣款、库存扣减)。
示例 SQL
sql
-- 开启事务
START TRANSACTION;
-- 查询数据并加排他锁,其他事务无法修改/加锁,只能阻塞
SELECT * FROM product_stock WHERE id = 1 FOR UPDATE;
-- 执行业务更新操作
UPDATE product_stock SET stock = stock - 1 WHERE id = 1;
-- 提交事务,锁自动释放
COMMIT;
关键说明
- 必须基于InnoDB 引擎 ,且查询条件命中索引,否则会升级为表锁,极大影响并发;
- 锁会持有到事务提交 / 回滚,事务越短,锁持有时间越短,性能越好。
方案 2:Java 原生同步锁(JVM 层面)
Java 提供的synchronized关键字、ReentrantLock可重入锁,是 JVM 进程内的悲观锁实现,控制多线程并发访问。
补充:synchronized 关键字
修饰方法 / 代码块,JVM 自动管理加锁 / 释放,使用更简单,适合简单并发场景。
方案 3:分布式悲观锁(分布式系统)
分布式场景下,JVM 本地锁失效,使用Redis 分布式锁(Redlock) 、ZooKeeper 分布式锁实现跨服务的悲观锁,核心是抢占唯一锁资源,未抢占到的请求阻塞 / 轮询。
(二)乐观锁实现方案
乐观锁没有真正的加锁操作 ,核心是版本校验机制,主流实现有两种:
方案 1:版本号机制(最常用,数据库优先)
- 数据表增加
version字段(整型),作为数据版本标识; - 查询数据时,同步查询版本号;
- 更新数据时,同时校验版本号,且将版本号 + 1;
- 若更新影响行数 = 0,说明数据已被修改,冲突发生。
实战步骤
- 建表语句(增加 version 字段)
java
CREATE TABLE product_stock (
id INT PRIMARY KEY AUTO_INCREMENT,
product_name VARCHAR(32),
stock INT,
version INT NOT NULL DEFAULT 0 COMMENT '数据版本号'
);
-
业务流程(Java+SQL)
-
第一步:查询数据和版本号
sqlSELECT id, stock, version FROM product_stock WHERE id = 1; -
第二步:更新时校验版本号(原子操作,数据库保证)
sqlUPDATE product_stock SET stock = stock - 1, version = version + 1 WHERE id = 1 AND version = ?; -- ? 为查询到的版本号
-
-
Java 业务层处理
java
public void optimisticLockUpdate(Long id) {
// 1. 查询数据和版本号
Stock stock = stockMapper.selectById(id);
// 2. 构建更新条件
LambdaUpdateWrapper<Stock> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(Stock::getId, id)
.eq(Stock::getVersion, stock.getVersion()); // 版本号校验
stock.setStock(stock.getStock() - 1);
stock.setVersion(stock.getVersion() + 1);
// 3. 执行更新,返回影响行数
int rows = stockMapper.update(stock, wrapper);
if (rows == 0) {
// 影响行数为0,说明版本不匹配,并发冲突
throw new RuntimeException("并发更新冲突,请重试");
}
}
方案 2:时间戳 / 字段值校验
不新增版本号字段,用数据更新时间戳 或原始字段值作为校验依据,逻辑与版本号一致,适合简单场景,精度略低。
方案 3:CAS 算法(JVM 层面,无锁并发)
Java java.util.concurrent.atomic包下的原子类(如AtomicInteger、AtomicLong),底层基于CAS(Compare And Swap,比较并交换) 实现乐观锁,是 CPU 指令级的原子操作,性能极高。示例:AtomicInteger 实现无锁扣减库存
关键说明
CAS 无需线程阻塞,通过自旋处理冲突,高并发下自旋次数过多会消耗 CPU。
方案 4:分布式乐观锁
分布式场景下,结合Redis 的 WATCH 命令 、版本号 + 分布式事务实现,适用于高并发分布式服务。
三、核心优缺点对比
| 维度 | 悲观锁 | 乐观锁 |
|---|---|---|
| 加锁时机 | 操作前加锁,全程持有 | 无锁操作,提交时校验 |
| 并发性能 | 低,阻塞等待,锁竞争激烈时性能差 | 高,无锁设计,适合高并发场景 |
| 冲突处理 | 自动阻塞,无需业务层处理 | 冲突返回失败,需业务层实现重试 / 报错逻辑 |
| 死锁风险 | 存在(未正确释放锁、长事务导致) | 无,无锁机制不存在死锁 |
| 数据一致性 | 强一致性,事务内数据独占 | 最终一致性,冲突时通过重试保证数据正确 |
| 实现复杂度 | 较低,数据库 / JDK 原生支持 | 中等,需要额外设计版本号 / 重试逻辑 |
四、适用场景选型
悲观锁适用场景
- 读少写多:写操作频繁,冲突概率极高,乐观锁会产生大量重试,反而降低性能;
- 强一致性要求 :金融交易、资金转账等不允许数据冲突的核心业务;
- 短事务操作:锁持有时间短,避免长时间阻塞其他请求;
- 单体应用 / 低并发系统:并发量小,实现简单,无需复杂的重试逻辑。
乐观锁适用场景
- 读多写少:读操作占比高,写冲突概率极低,能最大化提升系统并发能力;
- 高并发场景:电商商品库存、热点数据查询更新、秒杀系统等;
- 追求高性能:允许短暂的不一致,通过重试保证最终数据正确;
- JVM 本地无锁操作:使用原子类处理基础数据类型的并发修改。
五、CAS 存在的三大核心问题
问题 1:高并发下的自旋开销(最常见问题)
问题描述
CAS 失败时不会让线程阻塞挂起,而是通过自旋(循环重试) 不断尝试更新操作。
- 低并发场景:重试次数少,性能损耗可忽略;
- 高并发场景:大量线程同时竞争同一个变量,会出现大量自旋重试,长时间占用 CPU 资源,大幅降低系统性能。
解决方案
- 限制自旋次数 :放弃无限自旋,改为有限次数自旋 + 超时机制 ,超过阈值后线程挂起 / 抛出异常,避免 CPU 空转(如 JUC 中的
LongAdder替代AtomicLong); - 分段锁思想优化 :
LongAdder将热点数据拆分为多个分段,不同线程操作不同分段,最后汇总结果,大幅降低竞争概率; - 降级为阻塞锁 :自旋多次失败后,使用
LockSupport挂起线程,减少 CPU 占用。
问题 2:ABA 问题(经典逻辑漏洞)
问题描述
这是 CAS 的逻辑缺陷,执行流程:
- 线程 T1 读取变量值为 A;
- T1 被挂起,线程 T2 将变量值改为 B ,随后又改回 A;
- T1 恢复执行,执行 CAS 时发现内存值依旧是 A,判定无修改,成功更新;
- 问题:变量实际已经被修改过,CAS 无法感知,可能导致业务逻辑错误。
典型业务场景
库存扣减场景:初始库存 = 100 (A)→线程 2 临时修改为 99 (B)→修正回 100 (A)→线程 1 无感知扣减,忽略了中间的异常修改记录。
解决方案
核心思路:给变量增加版本戳 / 时间戳,值 + 版本共同判断,版本只增不减,彻底规避 ABA 问题。
- Java 官方解决方案 :使用
AtomicStampedReference(带版本戳的原子引用类),同时存储对象值 和版本号; - 数据库层面 :沿用我们上一节讲的版本号乐观锁,用 version 字段替代单纯的值校验。
代码示例:AtomicStampedReference 解决 ABA
java
import java.util.concurrent.atomic.AtomicStampedReference;
public class SolveABA {
// 初始化值:100,初始版本号:0
private static final AtomicStampedReference<Integer> stock = new AtomicStampedReference<>(100, 0);
public static void main(String[] args) {
// 获取初始值和初始版本
int oldValue = stock.getReference();
int oldStamp = stock.getStamp();
// 模拟线程2执行ABA操作:100→99→100,版本同步递增
stock.compareAndSet(100, 99, oldStamp, oldStamp + 1);
stock.compareAndSet(99, 100, oldStamp + 1, oldStamp + 2);
// 线程1尝试CAS更新:仅值匹配,版本不匹配,更新失败
boolean success = stock.compareAndSet(oldValue, 80, oldStamp, oldStamp + 1);
System.out.println("更新结果:" + success); // 输出:false
System.out.println("当前版本:" + stock.getStamp()); // 输出:2
}
}
问题 3:仅能保证单个共享变量的原子性
问题描述
CAS 的原子操作仅针对单个内存变量 生效,无法同时对多个变量执行原子性校验与更新。
例如业务中需要同时修改账户余额 和积分,两个变量存在关联关系,必须保证原子性,否则会出现数据不一致,但 CAS 无法直接实现。
解决方案
- 封装为对象 :将多个变量封装成一个 Java 对象,使用
AtomicReference<自定义对象>实现对整个对象的 CAS 操作; - 使用锁机制 :对于复杂的多变量并发场景,降级使用
synchronized或ReentrantLock,保证代码块的原子性; - 分布式 / 数据库场景:通过事务保证多操作的原子性。
六、问题与解决方案总结表
| 问题类型 | 核心影响 | 适用场景影响 | 标准解决方案 |
|---|---|---|---|
| 自旋开销 | 高并发下 CPU 空转,性能暴跌 | 秒杀、热点数据更新 | LongAdder 替代、限制自旋次数、分段优化 |
| ABA 问题 | 逻辑漏洞,无法感知变量中间修改 | 依赖数据变更轨迹的业务 | AtomicStampedReference、版本号机制 |
| 单变量原子性 | 无法保证多变量联动更新原子性 | 多字段关联修改场景 | 封装对象 + AtomicReference、加锁、事务 |
