【大白话说Java面试题 第114题】【并发篇】第14题:说一下悲观锁的优点和缺点?

📌 微服务架构基于Spring Cloud Alibaba的分布式事务处理:Seata AT模式与Sentinel协同实现高并发下数据最终一致性

第14题:说一下悲观锁的优点和缺点?

📚 回答:

  • 核心考点 : 悲观锁是并发控制的"保守派",大厂面试不会只问"优缺点",而是深入考察 悲观锁的性能开销模型 (上下文切换成本 vs 自旋成本)、死锁的形成条件与排查手段 (四要素 + jstack 实战)、锁粒度对并发度的影响 (行锁 vs 表锁 vs 间隙锁)、以及 悲观锁在分布式环境下的演进(数据库悲观锁 → 分布式锁 → 事务消息)。面试官真正想判断的是:你是否能根据业务场景的冲突概率、一致性要求、延迟敏感度,做出合理的锁选型决策。
1. 悲观锁的本质与实现层次
  • 1.1 核心思想

    悲观锁假设"冲突是常态",在访问数据前就先加锁,确保同一时刻只有一个线程能操作共享资源。其他线程必须等待锁释放后才能继续。

    实现层次

    层次 实现方式 粒度 适用场景
    JVM 级 synchronized / ReentrantLock 对象/代码块 单机多线程
    数据库级 SELECT FOR UPDATE / UPDATE 行锁 行/页/表 单体应用
    分布式 Redis RedLock / ZooKeeper / etcd 逻辑资源 微服务/分布式事务
  • 1.2 悲观锁 vs 乐观锁的选型边界

    复制代码
    冲突概率评估:
    ├── < 5%(读多写少)→ 乐观锁(版本号/CAS)
    ├── 5%~30%(读写均衡)→ 混合策略(读乐观 + 写悲观)
    └── > 30%(写多读少)→ 悲观锁(SELECT FOR UPDATE / 分布式锁)
2. 悲观锁的优点深度分析
  • 2.1 强一致性保障------事务的 ACID 基石

    悲观锁通过物理阻塞确保事务的隔离性,天然满足 ACID:

    • 原子性:锁保护下的操作不可分割;
    • 隔离性:锁阻止了脏读、不可重复读、幻读(配合隔离级别);
    • 一致性:事务执行前后数据始终处于有效状态;
    • 持久性:锁释放时数据已持久化。

    典型场景:金融转账、库存扣减、订单状态流转------任何数据不一致都可能导致资损。

  • 2.2 实现简单------"拿来即用"

    悲观锁的 API 简洁,心智负担低:

    java 复制代码
    // synchronized:JVM 自动管理
    synchronized (account) {
        account.balance -= amount;
    }
    
    // ReentrantLock:显式控制
    lock.lock();
    try {
        account.balance -= amount;
    } finally {
        lock.unlock();
    }
    
    // 数据库:一行 SQL
    SELECT balance FROM account WHERE id = 1 FOR UPDATE;

    无需设计版本号、重试逻辑、冲突处理------框架和数据库已处理好一切。

  • 2.3 天然防写偏斜(Write Skew)

    乐观锁无法检测的写偏斜问题,悲观锁通过锁的范围直接避免:

    sql 复制代码
    -- 悲观锁:锁定所有相关行
    BEGIN;
    SELECT * FROM doctor_schedule WHERE date = '2024-01-01' FOR UPDATE;
    -- 锁定了所有医生的排班行
    UPDATE doctor_schedule SET on_duty = false WHERE doctor_id = 1;
    COMMIT;
    -- 其他事务无法修改任何被锁定的行,彻底避免写偏斜
  • 2.4 超时与降级机制

    现代悲观锁支持超时获取,避免无限等待:

    java 复制代码
    // ReentrantLock 超时获取
    if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
        try {
            // 执行业务
        } finally {
            lock.unlock();
        }
    } else {
        // 降级:返回缓存数据、放入 MQ 异步处理
        return cache.get(key);
    }
    sql 复制代码
    -- MySQL 锁等待超时
    SET innodb_lock_wait_timeout = 5;  -- 5 秒超时后回滚
3. 悲观锁的缺点深度分析
  • 3.1 性能开销------上下文切换的代价

    线程阻塞 → 唤醒涉及内核态上下文切换,成本极高:

    操作 耗时 说明
    用户态 CAS 自旋 ~10-100ns 纯 CPU 操作
    用户态 → 内核态切换 ~1-10μs 系统调用开销
    线程上下文切换 ~1-10ms 保存/恢复寄存器、栈、PC
    数据库行锁等待 ~10ms-数秒 网络 RTT + 锁持有者执行时间

    量化对比(1000 并发,各 100 万次操作):

    方案 总耗时 吞吐量 CPU 使用率
    无锁(纯读取) 1s 100万 QPS 50%
    CAS 自旋 2s 50万 QPS 90%
    悲观锁(低冲突) 5s 20万 QPS 30%(大量线程阻塞)
    悲观锁(高冲突) 50s+ 2万 QPS 20%(锁串行化)
  • 3.2 死锁风险------并发编程的噩梦

    死锁形成的四个必要条件

    条件 说明 破坏策略
    互斥 资源一次只能被一个线程占用 无法破坏(锁的本质)
    占有且等待 持有锁的同时请求新锁 一次性申请所有锁
    不可抢占 已持有的锁不能被强制释放 设置锁超时
    循环等待 线程间形成循环依赖 固定加锁顺序

    经典死锁示例

    java 复制代码
    // 线程 A:先锁账户 1,再锁账户 2
    synchronized (account1) {
        synchronized (account2) { transfer(a1→a2); }
    }
    
    // 线程 B:先锁账户 2,再锁账户 1
    synchronized (account2) {
        synchronized (account1) { transfer(a2→a1); }
    }
    // → 循环等待,死锁!

    排查手段

    bash 复制代码
    # 1. 找到 Java 进程
    jps -l
    
    # 2. 打印线程栈,搜索 "BLOCKED"
    jstack -l <pid> | grep -A 20 "BLOCKED"
    
    # 3. MySQL 查看死锁日志
    SHOW ENGINE INNODB STATUS;  -- 查看 LATEST DETECTED DEADLOCK
  • 3.3 锁粒度问题------并发度的隐形杀手

    锁粒度越粗,并发度越低:

    粒度 实现 并发度 适用场景
    行锁 SELECT FOR UPDATE 高(只锁一行) 单行更新
    间隙锁 Next-Key Lock 中(锁行+间隙) 范围查询防幻读
    页锁 数据库内部 B+ 树页操作
    表锁 LOCK TABLES 低(锁整张表) DDL 操作
    全局锁 FLUSH TABLES WITH READ LOCK 极低 全库备份

    锁升级陷阱

    sql 复制代码
    -- MySQL 行锁可能升级为表锁!
    UPDATE user SET status = 1 WHERE name = '张三';  -- name 无索引 → 全表扫描 → 表锁!
  • 3.4 不适合读多写少场景

    悲观锁的"一视同仁"策略:即使是只读操作也会阻塞写线程(共享锁 S 与排他锁 X 冲突):

    sql 复制代码
    -- 事务 A 持有共享锁
    SELECT * FROM product WHERE id = 1 LOCK IN SHARE MODE;  -- S 锁
    
    -- 事务 B 请求排他锁 → 阻塞!
    UPDATE product SET stock = stock - 1 WHERE id = 1;  -- X 锁,等待 A 释放

    读多写少场景下,大量读线程持有 S 锁,写线程被严重阻塞。

  • 3.5 分布式环境下的复杂性

    单机悲观锁(synchronized)无法跨 JVM,必须引入分布式锁:

    方案 优点 缺点 适用场景
    Redis RedLock 性能高 时钟漂移、脑裂风险 缓存、限流
    ZooKeeper CP 强一致 性能低、依赖 ZK 集群 配置中心、选主
    etcd 高可用 学习成本高 服务发现、分布式锁
    数据库唯一索引 简单可靠 性能差、无续期 低频互斥操作

    Redisson 分布式锁示例

    java 复制代码
    RLock lock = redisson.getLock("order:lock:123");
    try {
        lock.lock();  // 看门狗自动续期
        // 执行业务
    } finally {
        lock.unlock();
    }
4. 悲观锁的优化策略
  • 4.1 锁粒度细化

    java 复制代码
    // ❌ 错误:粗粒度锁
    synchronized (this) {
        // 处理 1000 个订单
        for (Order order : orders) { process(order); }
    }
    
    // ✅ 正确:细粒度锁(按订单 ID 分段)
    private final ConcurrentHashMap<Long, Object> locks = new ConcurrentHashMap<>();
    
    public void processOrder(Long orderId) {
        Object lock = locks.computeIfAbsent(orderId, k -> new Object());
        synchronized (lock) {
            // 只锁单个订单
        }
    }
  • 4.2 读写分离(ReadWriteLock)

    java 复制代码
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    
    public void read() {
        rwLock.readLock().lock();   // 多个读线程可同时进入
        try { /* 读取操作 */ } finally { rwLock.readLock().unlock(); }
    }
    
    public void write() {
        rwLock.writeLock().lock();  // 独占,阻塞所有读写
        try { /* 写入操作 */ } finally { rwLock.writeLock().unlock(); }
    }
  • 4.3 锁超时与降级

    java 复制代码
    // 数据库锁超时
    @Transactional
    @QueryHints({@QueryHint(name = "javax.persistence.lock.timeout", value = "3000")})
    public Account findByIdForUpdate(Long id) { ... }
    
    // 分布式锁超时降级
    if (!redisLock.tryLock("inventory", 100, TimeUnit.MILLISECONDS)) {
        // 降级:返回缓存数据或排队
        return inventoryCache.get(productId);
    }
  • 4.4 无锁化改造(读多写少场景)

    java 复制代码
    // CopyOnWriteArrayList:写时复制,读完全无锁
    private final CopyOnWriteArrayList<Config> configs = new CopyOnWriteArrayList<>();
    
    public List<Config> getConfigs() {
        return configs;  // 读:无锁,直接返回快照
    }
    
    public void updateConfig(Config config) {
        configs.add(config);  // 写:复制新数组,加锁修改
    }
5. 生产环境避坑指南
  • 5.1 严禁锁内调用外部服务

    java 复制代码
    // ❌ 致命错误:锁内调用 HTTP/RPC/数据库
    synchronized (lock) {
        // 网络抖动 5 秒 → 锁持有 5 秒 → 其他线程全部阻塞
        paymentService.charge(order);
    }
    
    // ✅ 正确:锁内只操作内存,外部调用放锁外
    PaymentResult result;
    synchronized (lock) {
        result = preparePayment(order);  // 纯内存计算
    }
    paymentService.charge(result);       // 锁外执行
  • 5.2 固定加锁顺序

    java 复制代码
    // ✅ 正确:按 ID 升序加锁,避免循环等待
    public void transfer(Account a, Account b, BigDecimal amount) {
        Account first = a.getId() < b.getId() ? a : b;
        Account second = a.getId() < b.getId() ? b : a;
        synchronized (first) {
            synchronized (second) {
                a.debit(amount);
                b.credit(amount);
            }
        }
    }
  • 5.3 监控锁等待时间

    java 复制代码
    // 通过 JMX 监控 ReentrantLock
    ReentrantLock lock = new ReentrantLock();
    System.out.println("队列长度: " + lock.getQueueLength());
    System.out.println("是否有等待线程: " + lock.hasQueuedThreads());
    System.out.println("持有锁的线程: " + lock.getOwner());
    
    // MySQL 监控
    SELECT * FROM information_schema.INNODB_LOCK_WAITS;
    SELECT * FROM information_schema.INNODB_TRX WHERE trx_state = 'LOCK WAIT';
  • 5.4 避免锁升级

    sql 复制代码
    -- 确保 WHERE 条件走索引,避免行锁升级为表锁
    EXPLAIN UPDATE user SET status = 1 WHERE id = 1;  -- 确认 type=range/eq_ref
  • 5.5 分布式锁的看门狗机制

    java 复制代码
    // Redisson 自动续期(默认 30 秒过期,每 10 秒续期)
    RLock lock = redisson.getLock("myLock");
    lock.lock();  // 业务执行时间超过 30 秒时自动续期
    // 避免业务未完成锁已过期,导致并发问题
6. 面试官追问与高分回答模板
  • 追问 1:"悲观锁有哪些优点和缺点?"

    • 低分回答:"优点是强一致性,缺点是性能差、可能死锁。"(没有量化分析)
    • 高分回答 : "悲观锁的优点有三个维度:
      1. 强一致性:通过物理阻塞保证 ACID,天然防写偏斜,适合金融转账等零容忍场景;
      2. 实现简单:synchronized、ReentrantLock、SELECT FOR UPDATE 拿来即用,无需设计版本号和重试逻辑;
      3. 超时降级:支持 tryLock 超时、数据库锁等待超时,避免无限阻塞。

      缺点同样有三个维度:

      1. 性能开销:上下文切换成本 ~1-10ms,高并发下吞吐量断崖下降;
      2. 死锁风险:循环等待、占有且等待、不可抢占、互斥四条件同时满足时死锁;
      3. 读多写少场景低效:读操作也会阻塞写,大量 S 锁导致 X 锁饥饿。

      选型核心:冲突概率 > 30% 或强一致性要求时用悲观锁,否则用乐观锁。"

  • 追问 2:"悲观锁和乐观锁怎么选?"

    • 高分回答 : "选型取决于三个指标:冲突概率、一致性要求、延迟敏感度。

      冲突概率

      • < 5%(读多写少):乐观锁(版本号/CAS),性能碾压;
      • 5%~30%(读写均衡):混合策略,读用乐观、写用悲观;
      • 30%(写多读少):悲观锁,避免重试风暴。

      一致性要求

      • 金融转账、库存扣减:悲观锁(SELECT FOR UPDATE)+ 事务;
      • 社交点赞、浏览计数:乐观锁或无锁。

      延迟敏感度

      • 用户同步等待:悲观锁 + 超时降级;
      • 可异步处理:乐观锁 + MQ 重试。"
  • 追问 3:"怎么排查死锁?"

    • 高分回答 : "排查死锁分三层:
      1. 预防层:固定加锁顺序(如按 ID 升序)、设置锁超时(tryLock/innodb_lock_wait_timeout)、一次性申请所有锁;
      2. 检测层
        • Java:jstack -l <pid> 搜索 'BLOCKED' 和 'waiting to lock',查看锁持有者和等待者;
        • MySQL:SHOW ENGINE INNODB STATUS 查看 LATEST DETECTED DEADLOCK,包含事务 ID、锁类型、等待的索引记录;
      3. 恢复层:JVM 无法自动恢复死锁,需 Kill 线程;MySQL 会自动检测死锁并回滚代价最小的事务。

      最佳实践:代码审查时检查所有 synchronized/ReentrantLock 的嵌套层级,确保加锁顺序一致。"

  • 追问 4:"数据库的行锁什么时候会升级为表锁?"

    • 高分回答 : "MySQL InnoDB 行锁升级为表锁的典型场景:
      1. 无索引更新UPDATE user SET status = 1 WHERE name = '张三'(name 无索引)→ 全表扫描 → 所有行加锁 → 效果等同于表锁;
      2. 索引失效:隐式类型转换、函数操作导致索引失效,退化为全表扫描;
      3. 锁超时lock_wait_timeout 到期后,InnoDB 可能选择升级锁粒度以加速;
      4. DDL 操作ALTER TABLE 自动获取表级 MDL 锁。

      避免方法:

      • 确保 WHERE 条件走索引(EXPLAIN 确认 type=range/eq_ref);
      • 避免在索引列上做函数操作(WHERE DATE(create_time) = '2024-01-01');
      • 大事务拆小,减少锁持有时间。"
  • 追问 5:"分布式环境下,悲观锁怎么实现?"

    • 高分回答 : "单机悲观锁(synchronized)无法跨 JVM,分布式环境下有三种方案:
      1. 数据库悲观锁SELECT FOR UPDATE,简单但性能差、无法跨库;
      2. Redis 分布式锁 :Redisson 的 RLock,支持看门狗自动续期、可重入、红锁(RedLock)多主节点部署。缺点是依赖时钟同步,脑裂风险;
      3. ZooKeeper/etcd:基于临时顺序节点的分布式锁,CP 强一致,可靠性高但性能低。

      选型建议:

      • 缓存、限流等高频低持锁场景:Redis Redisson;
      • 配置中心、选主等强一致场景:ZooKeeper/etcd;
      • 低频简单互斥:数据库唯一索引。

      注意:分布式锁必须解决三个问题------互斥 (只能一个客户端持有)、防死锁 (过期自动释放)、可重入(同一线程多次获取)。"

  • 追问 6:"悲观锁在微服务架构下有什么问题?"

    • 高分回答 : "微服务架构下悲观锁面临三个挑战:
      1. 跨服务锁:订单服务和库存服务各用数据库悲观锁,无法协调。需要引入分布式事务(Seata AT/XA)或分布式锁;
      2. 数据库连接池耗尽:长事务持有锁,连接不释放,导致连接池耗尽。应缩短事务范围,锁内不调用外部服务;
      3. 锁粒度与分片:分库分表后,同一逻辑表的行锁分散在不同物理库,无法全局协调。需按分片键路由,或引入全局锁服务。

      现代方案:Saga 模式(最终一致性)+ 本地事务,替代全局悲观锁,提升吞吐量。"

7. 方案选型速查表
场景 冲突概率 一致性要求 推荐方案 不推荐方案
金融转账 强一致 悲观锁(SELECT FOR UPDATE)+ 事务 乐观锁
秒杀库存扣减 极高 强一致 Redis 预减 + MQ 异步 + 数据库悲观锁兜底 纯数据库悲观锁
商品详情页 极低 最终一致 无锁 + 缓存 任何锁
订单状态流转 强一致 悲观锁(行锁) 乐观锁
配置中心读取 极低 最终一致 CopyOnWriteArrayList 读写锁
用户积分累加 最终一致 乐观锁 + MQ 重试 悲观锁
分布式任务调度 强一致 ZooKeeper 分布式锁 Redis 锁(时钟漂移)
接口限流 最终一致 Redis Lua 原子脚本 数据库锁

💡 面试官想要的满分总结

悲观锁是并发控制的"保守派",其核心优势是强一致性和实现简单 ,核心代价是性能开销和死锁风险

选型的唯一金标准是冲突概率 + 一致性要求:金融转账等强一致场景必须用悲观锁(或分布式事务),读多写少场景应果断放弃悲观锁转向乐观锁或无锁。

工程实践中,悲观锁必须配合锁粒度细化 (行锁替代表锁)、固定加锁顺序 (防死锁)、超时降级 (防阻塞)、锁内零 IO(防连接池耗尽)四大原则。分布式环境下,数据库悲观锁需升级为 Redisson 或 ZooKeeper 分布式锁,并解决互斥、防死锁、可重入三大问题。

最后记住:悲观锁不是"万能锁",锁的持有时间越长,并发度越低。最好的优化不是"加更好的锁",而是"减少需要锁保护的代码"。


觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯

相关推荐
盒马盒马1 小时前
Rust:Vec
开发语言·rust
让我上个超影吧1 小时前
Claude Code 源码看 Agent 系统设计
java·ai·ai编程
plainGeekDev1 小时前
网络状态监听 → ConnectivityManager + Flow
android·java·kotlin
devilnumber1 小时前
Java 迭代器(Iterator)完全指南:从入门到实战
java·开发语言·迭代器
罗超驿1 小时前
13.Java多线程进阶:手动实现线程池与定时器机制详解
开发语言·面试·javaee
qq_195821651 小时前
6. 应用层协议实现:CoE协议栈集成、对象字典配置、PDO映射
java·服务器·网络
弹简特1 小时前
【Java项目-轻聊】10-实现会话管理模块
java·开发语言·数据库
人道领域1 小时前
Java后端开发者转型AIAgent开发路线指南
java·开发语言
uhakadotcom1 小时前
结合着 fastapi 使用,anyio 通常可以如何使用 , 它和 uvloop 在性能上有啥差异
后端·面试·github