Java 锁专题:从 synchronized 到分布式锁
锁是并发编程里最容易混的主题。
一开始学的是 synchronized 和 ReentrantLock,后来又遇到乐观锁、悲观锁、读写锁、分布式锁。名字都叫锁,但解决的问题并不完全一样。
这篇专门把锁相关内容从并发主线里拆出来,作为延伸阅读。
建议先看主线文章:
CAS 相关内容单独看:Java CAS 专题:从 AtomicInteger 到乐观并发。
锁到底解决什么问题
锁的核心作用是控制并发访问。
当多个线程或多个请求同时操作同一份资源时,如果不做控制,就可能出现数据错乱。
比如库存扣减:
库存 = 1
请求A读取库存:1请求B读取库存:1请求A扣减成功请求B扣减成功锁要解决的就是:同一时刻只能有一个执行者进入关键区域,或者更新时必须确认数据没有被别人改过。
synchronized
synchronized 是 Java 最基础的锁。
示例:
public class StockService {
private int stock = 10;
public synchronized void deduct() { if (stock > 0) { stock--; } }}它可以修饰方法,也可以修饰代码块。
public void deduct() { synchronized (this) { if (stock > 0) { stock--; } }}synchronized 的特点:
- 使用简单;
- JVM 层面支持;
- 自动释放锁;
- 出现异常时也会释放锁;
- 不需要手动 unlock。
适合临界区简单、锁竞争不复杂的场景。
ReentrantLock
ReentrantLock 是 JUC 提供的锁。
private final ReentrantLock lock = new ReentrantLock();
public void deduct() { lock.lock(); try { if (stock > 0) { stock--; } } finally { lock.unlock(); }}使用 ReentrantLock 时,必须在 finally 里释放锁。
否则一旦业务代码抛异常,锁不会释放,后续线程会一直等待。
相比 synchronized,ReentrantLock 更灵活:
- 可以尝试加锁:
tryLock(); - 可以设置等待时间;
- 可以响应中断;
- 可以创建公平锁;
- 可以配合多个
Condition。
synchronized 和 ReentrantLock 怎么选
简单场景优先 synchronized。
需要更强控制能力时再考虑 ReentrantLock。
| 对比 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM | JDK |
| 释放锁 | 自动释放 | 手动释放 |
| 可中断 | 不方便 | 支持 |
| 尝试加锁 | 不支持 | 支持 tryLock |
| 公平锁 | 不支持配置 | 支持 |
| 使用复杂度 | 低 | 稍高 |
不要为了“看起来高级”强行用 ReentrantLock。如果只是简单互斥,synchronized 足够清晰。
公平锁和非公平锁
公平锁指线程按照等待顺序获取锁。
非公平锁指新来的线程可以直接参与竞争,不一定排队。
ReentrantLock fairLock = new ReentrantLock(true);ReentrantLock unfairLock = new ReentrantLock(false);公平锁更符合直觉,但吞吐量通常低一些。
非公平锁可能让某些线程等得久一点,但整体性能往往更好。
生产环境默认一般使用非公平锁,除非业务真的要求严格排队。
读写锁
有些场景是读多写少,比如配置缓存、商品分类缓存。
多个线程同时读没问题,但写的时候不能有其他线程读写。
这时可以使用读写锁:
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();private final Lock readLock = rwLock.readLock();private final Lock writeLock = rwLock.writeLock();读操作:
readLock.lock();try { return cache.get(key);} finally { readLock.unlock();}写操作:
writeLock.lock();try { cache.put(key, value);} finally { writeLock.unlock();}读写锁适合读多写少。写很多的场景不一定划算。
悲观锁
悲观锁的思想是:我认为一定会有人和我抢,所以先锁住资源。
MySQL 里常见写法:
SELECT stockFROM productWHERE id = #{productId}FOR UPDATE;注意:FOR UPDATE 必须放在事务里才有意义。
@Transactionalpublic void createOrder(Long productId) { Product product = productMapper.selectForUpdate(productId); if (product.getStock() <= 0) { throw new RuntimeException("库存不足"); } productMapper.decreaseStock(productId); orderMapper.insert(...);}悲观锁适合强一致场景,但要注意:
- 事务不能太长;
- 锁住数据后不要调用外部接口;
- SQL 条件要命中索引;
- 多个资源要按固定顺序加锁,减少死锁。
乐观锁
乐观锁的思想是:先不加锁,更新时检查数据有没有被别人改过。
常见做法是在表里加 version 字段:
UPDATE productSET stock = stock - 1, version = version + 1WHERE id = #{productId} AND stock > 0 AND version = #{version};如果影响行数是 0,说明库存不足,或者版本号已经被别人改过。
乐观锁适合:
- 并发冲突不高;
- 可以接受失败重试;
- 不想长时间持有锁。
不适合:
- 秒杀这种大量请求抢同一条数据;
- 重试成本很高的业务;
- 失败后无法安全重试的业务。
乐观锁和 CAS 思想很接近,底层思路可以继续看:Java CAS 专题:从 AtomicInteger 到乐观并发。
分布式锁
单机锁只能管住一个 JVM。
如果服务部署了多个实例:
用户请求 ↓负载均衡 ↓服务A / 服务B / 服务C这时 synchronized 和 ReentrantLock 只能锁住当前实例,管不住其他实例。
这种场景可以考虑分布式锁,比如 Redis 锁。
简化版 Redis 加锁:
SET lock:order:1001 randomValue NX EX 10含义:
NX:只有 key 不存在时才设置成功;EX 10:10 秒后自动过期,避免死锁;randomValue:释放锁时校验,防止误删别人的锁。
释放锁时不能直接 DEL key,要先判断 value 是不是自己加的锁。真实项目一般用 Lua 脚本保证判断和删除的原子性。
分布式锁不是万能的
分布式锁适合:
- 控制定时任务只在一个节点执行;
- 防止同一业务对象被多个服务实例同时处理;
- 控制某些高风险资源的串行访问。
但不要什么都上分布式锁。
很多问题用下面这些方案更合适:
- 数据库唯一索引;
- 幂等表;
- 状态机;
- 条件更新 SQL;
- MQ 顺序消费;
- Redis token 防重复提交。
锁越重,系统吞吐量越容易下降。能用数据约束解决的,不一定要靠锁。
死锁怎么避免
死锁通常来自多个资源加锁顺序不一致。
线程A:先锁订单,再锁库存线程B:先锁库存,再锁订单避免死锁的常见方法:
- 统一加锁顺序;
- 减少锁嵌套;
- 缩短事务时间;
- 持锁期间不调用外部接口;
- 使用
tryLock设置等待时间; - 数据库 SQL 条件命中索引;
- 出现死锁后保留完整日志和事务 SQL。
业务里尤其要警惕长事务。
如果一个事务里先锁住库存,再去调用支付、物流、营销接口,就很容易把锁持有时间拖长,最终导致锁等待甚至死锁。
怎么选锁
可以先按这个思路选:
| 场景 | 建议 |
|---|---|
| 单机简单互斥 | synchronized |
| 需要 tryLock、可中断、公平锁 | ReentrantLock |
| 读多写少 | ReentrantReadWriteLock |
| 数据库强一致更新 | 悲观锁 / 条件更新 |
| 冲突不高,可重试 | 乐观锁 |
| 多实例抢同一资源 | Redis 分布式锁 |
| 防重复插入 | 数据库唯一索引优先 |
| 状态流转控制 | 状态机条件更新优先 |
总结
锁不是越多越安全,也不是越高级越好。
用锁前先问几个问题:
要保护的共享资源是什么?冲突是在单 JVM 内,还是多个服务实例之间?业务能不能失败重试?是否可以用唯一索引或状态机替代锁?持锁时间会不会很长?锁的本质是用吞吐量换一致性。真正难的是找到刚好够用的控制方式。
If this article helped you, please share it with others!
Some information may be outdated






