1. 从"防呆"到"防错":锁到底在防止什么东西被破坏
做开发这些年,几乎每个人都是从"线程不安全"这个模糊概念开始接触锁的。面试时背八股,能说出 synchronized、ReentrantLock、CAS,可真回到生产环境,看着监控里偶尔冒尖的请求耗时,或者日志中某条 SQL 突然堵在“Waiting for table metadata lock”,很多人还是会有一种"明明加了锁,为什么还是出事"的困惑。
我自己的体会是:锁机制真正要解决的核心问题,不是"防止两个线程同时执行",而是"防止两个线程对同一份共享数据做出一致性之外的交叉操作"。这句话听起来绕,落到场景里其实非常简单。
设想一个经典的扣库存场景。伪代码是这个样子:
text复制if (stock.count > 0) {
stock.count = stock.count - 1;
order.create();
}
单线程下这段逻辑没有任何问题。两个线程同时进来的时候,问题就开始暴露:线程 A 先读到了 stock.count 等于 1,线程 B 也读到了 stock.count 等于 1,A 判断大于 0,执行减一,B 判断也大于 0,也执行减一。最终库存变成 -1,但两个订单都创建成功了。
这段代码暴露的是三类典型的竞态条件:
- check-then-act(先检查后执行):判断库存是否充足再扣减,检查和扣减之间被插入了一个时间窗。
- read-modify-write(读改写):读取 count、内存中减 1、写回 count,三步不是原子的。
- 复合操作被拆散:判断、扣减、创建订单本来应该是一个整体语义,拆开后各自为政。
锁的职责,就是把这个时间窗封死。无论是 Java 里的 synchronized、Go 里的 mutex、MySQL 里的行锁,它们在做的事情本质上都一样:划定一块临界区,让同一时刻只有一个执行流能够进入。
这里有个非常容易被新手误解的点:锁并不保证"两个线程不能同时在 CPU 上跑"。多核时代,线程 A 在线程 B 运行的同时完全可以在另一个核上执行。锁保证的是,当 A 在临界区内写共享数据时,B 即使同时在跑,也无法读到 A 写到一半的中间状态,也无法同时写同一块数据。也就是说,锁解决的是"可见性"和"原子性"两个维度的问题,而不是"并行度"的问题。
明白了这一层,再看任何语言里的 Lock 接口或者数据库的事务隔离级别,思路都清晰得多。你选择的不是"某把具体的锁",而是"一种让并发操作可以线性化的策略"。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 一行 lock 背后是三层拼图:CPU、编译器和语言运行时
很多教程会直接告诉你"用 synchronized 加锁",然后就用 javap 看字节码里出现了 monitorenter / monitorexit。但如果你只停在这一层,遇到多线程性能问题或者诡异的指令重排 bug 时,依然会无从下手。
我想把这层窗户纸捅破:你在代码里写的这一行 lock,真正生效依赖了三层各自的承诺。
先看 CPU 层。锁最终要落地到一个硬件原语上。以 x86 为例,现代 CPU 提供了一条带有 lock 前缀的指令,比如 lock cmpxchg。这条指令在被执行时,会做两件事:一是锁住总线或缓存行,保证这个比较并交换的操作在
