并发里最危险的不是没加锁,而是:
加了锁,却不知道它会让系统慢到哪里、卡在哪里、崩在哪里。

所以本文不背概念,而围绕三个工程问题展开:
锁解决什么问题?代价是什么?如何取舍?
并补齐两件常被忽略的能力:可观测性可恢复性


锁的本质是一种“资源排队机制”

你可以把锁理解成系统里的“收费站”:

  • 解决的问题:避免车辆撞车(共享数据一致性)

  • 付出的代价:排队等待(吞吐下降、延迟上升)

  • 工程目标:收费站要少、要短、要可控(最小化临界区、减少竞争、可降级)

一句话总结:

锁不是为了让代码正确,而是为了让系统在并发下“可控地正确”。


1. 锁解决什么问题:共享可变状态的一致性

并发 bug 的根源从来不是“多线程很难”,而是两件事叠加:

  1. 共享(Shared):多个线程访问同一份数据

  2. 可变(Mutable):这份数据会被修改

最经典的例子:库存扣减/抢票超卖。

为什么会超卖?因为扣库存不是一步:

扣库存 = 读(stock) → 判断(stock>0) → 写(stock-1)

这是一个“复合操作”,天然不是原子性的。

所以锁的意义就是把这段复合操作包成一个整体:

把临界区串行化:同一时刻只有一个线程能做读改写。


2. 锁的代价:性能、延迟、可维护性三杀

2.1 性能成本:阻塞/唤醒 + 上下文切换

线程阻塞与唤醒需要 OS 介入,CPU 状态切换也会造成 cache 失效。

很多时候临界区只有几行代码,但阻塞/唤醒的代价可能更大——这就是自旋锁出现的原因。

2.2 延迟成本:长尾比平均更致命

锁竞争越激烈,排队越长,P99 延迟会急剧恶化。线上很多事故不是平均变慢,而是少量请求卡死导致雪崩。

2.3 可维护性成本:排障难、治理难

锁引发的问题往往不是“报错”,而是“卡住”。卡住意味着:

  • 没异常

  • CPU 不高

  • 线程池却满了

  • RT 爆炸

这类问题如果缺乏可观测性,排查会非常痛苦。

所以我一直强调:

锁不只是代码结构,它是一个需要治理的系统组件。


3. 乐观锁 vs 悲观锁:本质是“冲突概率假设”

别把乐观/悲观当哲学,它们只是两种成本模型:

3.1 悲观锁(synchronized / Lock)

  • 假设:一定会冲突

  • 策略:先加锁再操作

  • 成本:排队阻塞(线程等待)

适合:

  • 写多

  • 冲突高

  • 强一致

典型:库存、余额、订单状态。

3.2 乐观锁(CAS / Atomic)

  • 假设:大概率不冲突

  • 策略:提交时 CAS 校验

  • 成本:冲突时自旋重试(耗 CPU)

适合:

  • 读多写少

  • 冲突低

  • 可重试

典型:计数器、统计指标。

悲观锁:用等待换正确
乐观锁:用重试换正确


4. 自旋锁 vs 阻塞锁:用 CPU 换延迟

自旋锁本质是:

我先不睡,盯着锁看一会儿,万一马上释放就赚了。

适用边界很明确:

  • 临界区很短:自旋划算

  • 临界区很长(IO/RPC/DB):自旋浪费 CPU

工程建议:

锁内出现 IO/RPC/DB,就不要幻想自旋能救你。


5. JVM 锁升级:无锁→偏向→轻量→重量(只记工程含义)

很多文章讲对象头、Mark Word,我建议你在工程上只记一句话:

synchronized 并不等于重量级锁,JVM 会根据竞争程度自动升级。

  • 偏向锁:几乎单线程

  • 轻量级锁:少量竞争,CAS 自旋

  • 重量级锁:竞争激烈,阻塞等待

所以不要一上来就“恐惧 synchronized”,真正让系统慢的永远是:

锁竞争 + 临界区过大


6. 代码示例:库存扣减(含超时降级 + 可观测性)

这是更接近生产的写法:既保证正确性,又能自我保护。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class StockService {
    private int stock = 10;

    // 非公平锁:吞吐更高(秒杀更常用)
    private final Lock lock = new ReentrantLock(false);

    public boolean buy(String userId) throws InterruptedException {
        long start = System.currentTimeMillis();

        // 可恢复性:超时失败,避免线程堆积
        if (!lock.tryLock(50, TimeUnit.MILLISECONDS)) {
            System.out.printf("buy fail(user=%s): lock timeout%n", userId);
            return false;
        }

        try {
            // 临界区最小化:只保护共享变量读改写
            if (stock <= 0) return false;
            stock--;
            return true;
        } finally {
            lock.unlock();
            long cost = System.currentTimeMillis() - start;
            // 可观测性:记录锁持有耗时(线上排障很关键)
            if (cost > 20) {
                System.out.printf("buy slow(user=%s): lockCost=%dms%n", userId, cost);
            }
        }
    }
}

这段代码体现了我认为锁必须具备的两种能力:

  • 可恢复性:tryLock 超时,失败快速返回

  • 可观测性:记录锁耗时,定位慢在哪里


7. synchronized vs ReentrantLock:场景化对比(不是背答案)

维度 synchronized ReentrantLock
可重入 支持 支持
可中断 不支持(等待锁不可中断) 支持 lockInterruptibly
超时 不支持 支持 tryLock(timeout)
公平性 非公平 可公平/非公平
可观测性 主要靠 jstack 可查询队列/持锁状态
使用成本 语法简单,自动释放 必须手动 unlock(易忘)

怎么选?给你一个工程口诀:

  • 临界区短、逻辑简单、无需超时synchronized

  • 高并发竞争、需要超时/中断/公平、要治理ReentrantLock

我个人更偏向的观点:

synchronized 更像“语言级安全带”,
ReentrantLock 更像“工程级控制系统”。


8. 死锁:锁机制里最贵的坑(如何避免 + 如何排查)

8.1 死锁的典型模式:锁顺序不一致

线程 A:锁 A → 锁 B
线程 B:锁 B → 锁 A
两个线程互相等待,永远卡死。

8.2 工程化避免死锁的三板斧

  1. 统一加锁顺序(制度化,不靠自觉)

  2. 避免锁嵌套(能拆就拆)

  3. 超时兜底tryLock(timeout),拿不到就降级/回滚

这就是“可恢复性”的价值:
系统宁可失败,也不要卡死。

8.3 排查死锁:别猜,直接看线程栈

  • jstack pid | grep -A 20 deadlock

  • Arthas:thread -b(查看阻塞线程)

排查关键点:

  • 谁持有锁?

  • 谁在等待锁?

  • 等了多久?

  • 锁内在做什么?(最常见:锁内 IO/RPC)


9. 锁的正确姿势:三少两要(我的个人总结)

三少

  1. 少锁代码:临界区最小化(只锁共享变量读改写)

  2. 少锁时间:锁内禁止 IO/RPC/DB

  3. 少锁竞争:拆锁、分段锁、读写分离

两要

  1. 要可观测性:日志/监控能定位锁等待与持锁耗时

  2. 要可恢复性:超时、降级、失败快速返回


10. 锁选型决策树(建议收藏)

你可以按这个顺序做选择:

1)是否必须强一致?

  • 否 → 优先无锁/最终一致(队列、异步、缓存)

  • 是 → 继续

2)冲突概率高吗?

  • 低 → CAS/Atomic/乐观锁

  • 高 → 悲观锁

3)锁失败能不能接受?

  • 能 → tryLock(timeout) + 降级

  • 不能 → 阻塞锁(但要监控)

4)读多写少吗?

  • 是 → 读写锁(ReentrantReadWriteLock)

  • 否 → 互斥锁


结尾:2 条实战建议(可直接落地)

建议 1:锁粒度优化(临界区最小化 + 分段锁)

  • 把锁内代码缩到“共享变量读改写”

  • 大对象锁拆小(按用户/商品/订单维度)

  • 热点 key 用分段锁降低竞争

收益: 吞吐上升,长尾下降,死锁概率下降。

建议 2:读写分离 + 原子类替代互斥锁

  • 计数/统计:优先 LongAdder

  • 读多写少:ReentrantReadWriteLock

  • 状态更新:CAS + 版本号解决 ABA

收益: 并发度提升,减少线程阻塞。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐