mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1769 words
5 minutes
Java 锁专题:从 synchronized 到分布式锁
2025-03-18

Java 锁专题:从 synchronized 到分布式锁#

锁是并发编程里最容易混的主题。

一开始学的是 synchronizedReentrantLock,后来又遇到乐观锁、悲观锁、读写锁、分布式锁。名字都叫锁,但解决的问题并不完全一样。

这篇专门把锁相关内容从并发主线里拆出来,作为延伸阅读。

建议先看主线文章:

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 里释放锁。

否则一旦业务代码抛异常,锁不会释放,后续线程会一直等待。

相比 synchronizedReentrantLock 更灵活:

  • 可以尝试加锁:tryLock()
  • 可以设置等待时间;
  • 可以响应中断;
  • 可以创建公平锁;
  • 可以配合多个 Condition

synchronized 和 ReentrantLock 怎么选#

简单场景优先 synchronized

需要更强控制能力时再考虑 ReentrantLock

对比synchronizedReentrantLock
实现层面JVMJDK
释放锁自动释放手动释放
可中断不方便支持
尝试加锁不支持支持 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 stock
FROM product
WHERE id = #{productId}
FOR UPDATE;

注意:FOR UPDATE 必须放在事务里才有意义。

@Transactional
public void createOrder(Long productId) {
Product product = productMapper.selectForUpdate(productId);
if (product.getStock() <= 0) {
throw new RuntimeException("库存不足");
}
productMapper.decreaseStock(productId);
orderMapper.insert(...);
}

悲观锁适合强一致场景,但要注意:

  • 事务不能太长;
  • 锁住数据后不要调用外部接口;
  • SQL 条件要命中索引;
  • 多个资源要按固定顺序加锁,减少死锁。

乐观锁#

乐观锁的思想是:先不加锁,更新时检查数据有没有被别人改过。

常见做法是在表里加 version 字段:

UPDATE product
SET stock = stock - 1,
version = version + 1
WHERE id = #{productId}
AND stock > 0
AND version = #{version};

如果影响行数是 0,说明库存不足,或者版本号已经被别人改过。

乐观锁适合:

  • 并发冲突不高;
  • 可以接受失败重试;
  • 不想长时间持有锁。

不适合:

  • 秒杀这种大量请求抢同一条数据;
  • 重试成本很高的业务;
  • 失败后无法安全重试的业务。

乐观锁和 CAS 思想很接近,底层思路可以继续看:Java CAS 专题:从 AtomicInteger 到乐观并发

分布式锁#

单机锁只能管住一个 JVM。

如果服务部署了多个实例:

用户请求
负载均衡
服务A / 服务B / 服务C

这时 synchronizedReentrantLock 只能锁住当前实例,管不住其他实例。

这种场景可以考虑分布式锁,比如 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 内,还是多个服务实例之间?
业务能不能失败重试?
是否可以用唯一索引或状态机替代锁?
持锁时间会不会很长?

锁的本质是用吞吐量换一致性。真正难的是找到刚好够用的控制方式。

Share

If this article helped you, please share it with others!

Java 锁专题:从 synchronized 到分布式锁
https://mizuki.mysqil.com/posts/java-locks/
Author
梦幻晨风
Published at
2025-03-18
License
CC BY-NC-SA 4.0

Some information may be outdated

Table of Contents