从零起步学习并发编程 || 第五章:悲观锁与乐观锁的思想与实现及实战应用与问题

一、核心概念与设计思想

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;
关键说明
  1. 必须基于InnoDB 引擎 ,且查询条件命中索引,否则会升级为表锁,极大影响并发;
  2. 锁会持有到事务提交 / 回滚,事务越短,锁持有时间越短,性能越好

方案 2:Java 原生同步锁(JVM 层面)

Java 提供的synchronized关键字、ReentrantLock可重入锁,是 JVM 进程内的悲观锁实现,控制多线程并发访问。

补充:synchronized 关键字

修饰方法 / 代码块,JVM 自动管理加锁 / 释放,使用更简单,适合简单并发场景。

方案 3:分布式悲观锁(分布式系统)

分布式场景下,JVM 本地锁失效,使用Redis 分布式锁(Redlock)ZooKeeper 分布式锁实现跨服务的悲观锁,核心是抢占唯一锁资源,未抢占到的请求阻塞 / 轮询。

(二)乐观锁实现方案

乐观锁没有真正的加锁操作 ,核心是版本校验机制,主流实现有两种:

方案 1:版本号机制(最常用,数据库优先)

  1. 数据表增加version字段(整型),作为数据版本标识;
  2. 查询数据时,同步查询版本号;
  3. 更新数据时,同时校验版本号,且将版本号 + 1;
  4. 若更新影响行数 = 0,说明数据已被修改,冲突发生。
实战步骤
  1. 建表语句(增加 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 '数据版本号'
);
  1. 业务流程(Java+SQL)

    • 第一步:查询数据和版本号

      sql 复制代码
      SELECT id, stock, version FROM product_stock WHERE id = 1;
    • 第二步:更新时校验版本号(原子操作,数据库保证)

      sql 复制代码
      UPDATE product_stock 
      SET stock = stock - 1, version = version + 1 
      WHERE id = 1 AND version = ?; -- ? 为查询到的版本号
  2. 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包下的原子类(如AtomicIntegerAtomicLong),底层基于CAS(Compare And Swap,比较并交换) 实现乐观锁,是 CPU 指令级的原子操作,性能极高。示例:AtomicInteger 实现无锁扣减库存

关键说明

CAS 无需线程阻塞,通过自旋处理冲突,高并发下自旋次数过多会消耗 CPU。

方案 4:分布式乐观锁

分布式场景下,结合Redis 的 WATCH 命令版本号 + 分布式事务实现,适用于高并发分布式服务。

三、核心优缺点对比

维度 悲观锁 乐观锁
加锁时机 操作前加锁,全程持有 无锁操作,提交时校验
并发性能 低,阻塞等待,锁竞争激烈时性能差 高,无锁设计,适合高并发场景
冲突处理 自动阻塞,无需业务层处理 冲突返回失败,需业务层实现重试 / 报错逻辑
死锁风险 存在(未正确释放锁、长事务导致) 无,无锁机制不存在死锁
数据一致性 强一致性,事务内数据独占 最终一致性,冲突时通过重试保证数据正确
实现复杂度 较低,数据库 / JDK 原生支持 中等,需要额外设计版本号 / 重试逻辑

四、适用场景选型

悲观锁适用场景

  1. 读少写多:写操作频繁,冲突概率极高,乐观锁会产生大量重试,反而降低性能;
  2. 强一致性要求 :金融交易、资金转账等不允许数据冲突的核心业务;
  3. 短事务操作:锁持有时间短,避免长时间阻塞其他请求;
  4. 单体应用 / 低并发系统:并发量小,实现简单,无需复杂的重试逻辑。

乐观锁适用场景

  1. 读多写少:读操作占比高,写冲突概率极低,能最大化提升系统并发能力;
  2. 高并发场景:电商商品库存、热点数据查询更新、秒杀系统等;
  3. 追求高性能:允许短暂的不一致,通过重试保证最终数据正确;
  4. JVM 本地无锁操作:使用原子类处理基础数据类型的并发修改。

五、CAS 存在的三大核心问题

问题 1:高并发下的自旋开销(最常见问题)

问题描述

CAS 失败时不会让线程阻塞挂起,而是通过自旋(循环重试) 不断尝试更新操作。

  • 低并发场景:重试次数少,性能损耗可忽略;
  • 高并发场景:大量线程同时竞争同一个变量,会出现大量自旋重试,长时间占用 CPU 资源,大幅降低系统性能。

解决方案

  1. 限制自旋次数 :放弃无限自旋,改为有限次数自旋 + 超时机制 ,超过阈值后线程挂起 / 抛出异常,避免 CPU 空转(如 JUC 中的LongAdder替代AtomicLong);
  2. 分段锁思想优化LongAdder将热点数据拆分为多个分段,不同线程操作不同分段,最后汇总结果,大幅降低竞争概率;
  3. 降级为阻塞锁 :自旋多次失败后,使用LockSupport挂起线程,减少 CPU 占用。

问题 2:ABA 问题(经典逻辑漏洞)

问题描述

这是 CAS 的逻辑缺陷,执行流程:

  1. 线程 T1 读取变量值为 A
  2. T1 被挂起,线程 T2 将变量值改为 B ,随后又改回 A
  3. T1 恢复执行,执行 CAS 时发现内存值依旧是 A,判定无修改,成功更新;
  4. 问题:变量实际已经被修改过,CAS 无法感知,可能导致业务逻辑错误。

典型业务场景

库存扣减场景:初始库存 = 100 (A)→线程 2 临时修改为 99 (B)→修正回 100 (A)→线程 1 无感知扣减,忽略了中间的异常修改记录。

解决方案

核心思路:给变量增加版本戳 / 时间戳,值 + 版本共同判断,版本只增不减,彻底规避 ABA 问题。

  1. Java 官方解决方案 :使用AtomicStampedReference(带版本戳的原子引用类),同时存储对象值版本号
  2. 数据库层面 :沿用我们上一节讲的版本号乐观锁,用 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 无法直接实现。

解决方案

  1. 封装为对象 :将多个变量封装成一个 Java 对象,使用AtomicReference<自定义对象>实现对整个对象的 CAS 操作;
  2. 使用锁机制 :对于复杂的多变量并发场景,降级使用synchronizedReentrantLock,保证代码块的原子性;
  3. 分布式 / 数据库场景:通过事务保证多操作的原子性。

六、问题与解决方案总结表

问题类型 核心影响 适用场景影响 标准解决方案
自旋开销 高并发下 CPU 空转,性能暴跌 秒杀、热点数据更新 LongAdder 替代、限制自旋次数、分段优化
ABA 问题 逻辑漏洞,无法感知变量中间修改 依赖数据变更轨迹的业务 AtomicStampedReference、版本号机制
单变量原子性 无法保证多变量联动更新原子性 多字段关联修改场景 封装对象 + AtomicReference、加锁、事务
相关推荐
李小白202002022 小时前
EMMC写入/烧录逻辑
linux·运维·服务器
Volunteer Technology2 小时前
Sentinel的限流算法
java·python·算法
VX:Fegn08952 小时前
计算机毕业设计|基于springboot + vue云租车平台系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
岁岁种桃花儿2 小时前
SpringCloud从入门到上天:Nacos做微服务注册中心
java·spring cloud·微服务
jdyzzy2 小时前
什么是 JIT 精益生产模式?它与传统的生产管控方式有何不同?
java·大数据·人工智能·jit
Chasmれ2 小时前
Spring Boot 1.x(基于Spring 4)中使用Java 8实现Token
java·spring boot·spring
阿蒙Amon2 小时前
TypeScript学习-第13章:实战与最佳实践
javascript·学习·typescript
汤姆yu2 小时前
2026基于springboot的在线招聘系统
java·spring boot·后端
Elastic 中国社区官方博客2 小时前
跳过 MLOps:通过 Cloud Connect 使用 EIS 为自管理 Elasticsearch 提供托管云推理
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索