Java 锁机制入门:一篇文章搞懂悲观锁、乐观锁、分布式锁和看门狗
写给 Java 新手的锁机制指南,用最通俗的语言和代码,搞懂锁到底是干嘛的、怎么用。
目录
1. 为什么需要锁?
先看一个生活中的例子:
你和室友共用一个冰箱,里面只剩 1 瓶可乐。 你打开冰箱看到有可乐,去拿杯子。 同时室友也打开冰箱看到有可乐,也去拿杯子。 你回来拿走了可乐,室友回来发现可乐没了——傻眼了。
这就是超卖问题。在程序里也一样:
// 库存只剩 1 件 int stock = 1; // 线程A读到 stock=1 // 线程B也读到 stock=1 // 线程A扣减:stock = 0,下单成功 // 线程B扣减:stock = -1,也下单成功了!→ 超卖了! stock = stock - 1; // 两个线程同时执行这行,结果是0而不是-1?不一定!
锁就是解决这个问题的:同一时刻只允许一个人操作库存,其他人排队等。
2. 悲观锁:先上锁再干活
什么是悲观锁?
悲观锁的想法很"悲观":它认为只要有人来抢,就一定会出问题。所以它先锁住资源,操作完再放开,期间其他人只能等着。
就像你去上厕所,先把门锁上,用完再开门。别人看到门锁着就只能等。
代码示例
方式1:synchronized 关键字(最简单)
public class Warehouse {
private int stock = 100; // 库存100件
// synchronized 会让方法同一时间只能被一个线程执行
public synchronized boolean deduct(int quantity) {
if (stock >= quantity) {
stock -= quantity;
System.out.println("扣减成功,剩余库存: " + stock);
return true;
}
System.out.println("库存不足,当前库存: " + stock);
return false;
}
public static void main(String[] args) {
Warehouse warehouse = new Warehouse();
// 模拟100个人同时抢购
for (int i = 0; i < 100; i++) {
new Thread(() -> {
warehouse.deduct(1);
}).start();
}
// 结果:库存永远不会变成负数
}
}
简单理解:加了 synchronized 的方法,同一时间只能有一个人进去执行,其他人排队等。
方式2:ReentrantLock(更灵活)
import java.util.concurrent.locks.ReentrantLock;
public class Warehouse {
private int stock = 100;
private ReentrantLock lock = new ReentrantLock();
public boolean deduct(int quantity) {
lock.lock(); // 上锁
try {
if (stock >= quantity) {
stock -= quantity;
return true;
}
return false;
} finally {
lock.unlock(); // 一定要在 finally 里解锁,否则出异常就死锁了
}
}
}
ReentrantLock 比 synchronized 多了几个本事:
-
可以尝试获取锁,获取不到就不等了(
tryLock()) -
可以设置等待时间(等 3 秒获取不到就放弃)
-
可以中断等待(不想等了可以取消)
// 试试能不能拿到锁,拿不到就算了
if (lock.tryLock()) {
try {
// 拿到锁了,干活
} finally {
lock.unlock();
}
} else {
// 没拿到锁,做别的事情
}
// 最多等3秒
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
// 3秒内拿到了锁
} finally {
lock.unlock();
}
} else {
// 3秒都没拿到,放弃
}
方式3:数据库悲观锁(SELECT FOR UPDATE)
在数据库层面加锁,适合需要锁住数据库记录的场景。
-- 先锁定这条记录(别人改不了也删不了) BEGIN; SELECT stock FROM product WHERE id = 1 FOR UPDATE; -- 在锁住的状态下修改 UPDATE product SET stock = stock - 1 WHERE id = 1; COMMIT; -- 提交后锁才释放
@Transactional
public boolean deductStock(Long productId, int quantity) {
// SELECT FOR UPDATE 会锁住这行记录
Product product = productMapper.selectForUpdate(productId);
if (product.getStock() >= quantity) {
productMapper.updateStock(productId, product.getStock() - quantity);
return true;
}
return false;
}
悲观锁的特点:
-
一定会锁住资源,别人必须等
-
安全,不会超卖
-
性能稍差(要等锁)
-
适合写操作多的场景
3. 乐观锁:先干活,有冲突再重试
什么是乐观锁?
乐观锁的想法很"乐观":它认为大家同时抢同一件东西的概率很小。所以它先去操作,操作完再检查有没有人跟我抢过,如果有就重试。
就像你去食堂打饭,你看到红烧肉还有,直接说"来一份"。如果刚好被前面的人打完了,你就重新看看还有什么菜。
代码示例
方式1:CAS 机制(Compare And Swap)
CAS 是乐观锁的核心,意思是比较并交换:
CAS(内存地址, 期望值, 新值): 如果内存里的值 == 期望值,就改成新值(成功) 如果不等于,说明被人改过了,不改了(失败,重试)
import java.util.concurrent.atomic.AtomicInteger;
public class Warehouse {
// AtomicInteger 是 Java 提供的线程安全整数
private AtomicInteger stock = new AtomicInteger(100);
public boolean deduct(int quantity) {
while (true) {
int current = stock.get(); // 读取当前值
if (current < quantity) {
return false; // 库存不足
}
// CAS:如果当前值还是 current,就改成 current - quantity
if (stock.compareAndSet(current, current - quantity)) {
return true; // 扣减成功
}
// 如果失败了,说明有人抢先改了,循环重试
}
}
}
为什么不用加锁? 因为 compareAndSet 是 CPU 指令级别的原子操作,硬件保证同一时间只有一个线程能成功。
方式2:数据库乐观锁(版本号)
在表里加一个 version 字段,每次更新都检查版本号。
-- 表结构 CREATE TABLE product ( id BIGINT PRIMARY KEY, name VARCHAR(100), stock INT, version INT DEFAULT 0 -- 版本号 );
// 查询时把版本号读出来
Product product = productMapper.selectById(1);
// stock=100, version=0
// 更新时检查版本号是否还是0
int affected = productMapper.update(
"UPDATE product SET stock=stock-1, version=version+1 " +
"WHERE id=1 AND version=0"
);
if (affected > 0) {
// 更新成功,没人跟我抢
} else {
// affected=0,说明版本号变了,有人改过了,重试!
}
通俗理解:
-
你看到商品库存 100,版本号是 0
-
你准备扣减库存
-
更新的时候告诉数据库:"只有版本号还是 0 的时候才帮我改"
-
如果版本号被别人改成 1 了,说明有人抢先了,你就重试
乐观锁的特点:
-
不锁资源,先操作再检查
-
性能好(不用等锁)
-
适合读多写少、冲突少的场景
-
冲突多的时候会反复重试,反而浪费性能
4. 分布式锁:多台机器怎么办?
为什么需要分布式锁?
前面说的 synchronized 和 ReentrantLock 只在一台机器内有效。
现在你的系统部署在 3 台服务器上:
用户请求 → 负载均衡 → 服务器1(synchronized 锁住了) → 负载均衡 → 服务器2(也 synchronized,但这是另一把锁!) → 负载均衡 → 服务器3(也 synchronized,又是另一把锁!)
三台机器各锁各的,根本没用!库存还是会超卖。
分布式锁就是让所有服务器共用同一把锁,通常用 Redis 或 Zookeeper 实现。
方式1:Redis 分布式锁(用 Redisson)
Redisson 是操作 Redis 的 Java 工具,已经封装好了分布式锁。
Maven 依赖:
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.27.0</version> </dependency>
代码示例:
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class DistributedWarehouse {
private static RedissonClient redisson;
private int stock = 100;
static {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
redisson = Redisson.create(config);
}
public boolean deduct(int quantity) {
// 获取分布式锁(所有服务器共用这一把锁)
RLock lock = redisson.getLock("warehouse:stock");
lock.lock(); // 上锁
try {
if (stock >= quantity) {
stock -= quantity;
return true;
}
return false;
} finally {
lock.unlock(); // 解锁
}
}
}
原理很简单:往 Redis 里存一个 key 作为锁,谁存成功了谁就拿到锁。
服务器1:SET lock_key "我拿到了" NX → 成功!我干活 服务器2:SET lock_key "我拿到了" NX → 失败!key已存在,我等着 服务器3:SET lock_key "我拿到了" NX → 失败!key已存在,我等着 服务器1干完活:DEL lock_key → 锁释放了 服务器2:SET lock_key "我拿到了" NX → 成功!轮到我了
分布式锁的特点:
-
所有服务器共用一把锁
-
安全,不会超卖
-
有网络开销,性能比本地锁差
-
适合集群部署的场景
5. 看门狗机制:锁过期了业务还没做完怎么办?
问题:锁过期了
分布式锁通常会设置一个过期时间(比如 30 秒),防止服务器宕机后锁永远不释放(死锁)。
但问题是:如果业务执行超过 30 秒,锁自动过期了,其他服务器就进来了,这时候又会超卖!
服务器1:拿到锁,开始处理(预计30秒能完成) 第30秒:锁自动过期了!业务还没做完! 服务器2:拿到锁,也开始处理 结果:两台服务器同时在操作库存 → 超卖!
解决方案:看门狗
看门狗就像一个保安,每隔一段时间检查一下你干完活没有。如果没干完,就帮你把锁的过期时间延长。
服务器1:拿到锁(30秒过期) 第10秒:看门狗检查 → 还没干完 → 续期到30秒后 第20秒:看门狗检查 → 还没干完 → 续期到30秒后 第25秒:业务干完了 → 主动释放锁 → 看门狗下班
Redisson 的看门狗
好消息:Redisson 自带看门狗,你不需要自己实现!
public boolean deduct(int quantity) {
RLock lock = redisson.getLock("warehouse:stock");
// 不指定过期时间 → 自动启用看门狗
lock.lock();
try {
// 不管执行多久,锁都不会过期
// 看门狗每10秒续期一次,续期到30秒后
doSomeLongTimeWork();
return true;
} finally {
lock.unlock(); // 干完了释放锁
}
}
对比一下:
// 方式1:不指定过期时间 → 有看门狗,锁不会过期 lock.lock(); // 方式2:指定过期时间 → 没有看门狗,30秒后锁自动过期 lock.lock(30, TimeUnit.SECONDS); // 方式3:尝试获取锁,最多等5秒,10秒后过期 → 没有看门狗 lock.tryLock(5, 10, TimeUnit.SECONDS);
建议:
-
业务执行时间不确定 → 用
lock.lock(),让看门狗自动续期 -
业务执行时间很短且确定 → 用
lock.lock(10, TimeUnit.SECONDS),不需要看门狗
6. 一句话总结:什么时候用什么锁?
一张图看懂
需要加锁吗? │ ├── 单台机器就能搞定? │ ├── 是 → 用 synchronized 或 ReentrantLock(悲观锁) │ └── 读多写少?→ 用 ReadWriteLock │ └── 多台机器一起干? └── 是 → 用分布式锁(Redis/Zookeeper)
对比表
| 悲观锁 | 乐观锁 | 分布式锁 | |
|---|---|---|---|
| 一句话 | 先锁再干 | 先干再说 | 多台机器共用一把锁 |
| 实现 | synchronized、ReentrantLock、SELECT FOR UPDATE | CAS、Atomic、版本号 | Redis、Zookeeper |
| 性能 | 要等锁,稍慢 | 不等锁,快 | 有网络开销,最慢 |
| 安全 | 最安全 | 冲突少时安全 | 安全 |
| 适合场景 | 写操作多、冲突多 | 读多写少、冲突少 | 集群部署 |
| 典型场景 | 转账、扣库存 | 计数器、缓存更新 | 秒杀、分布式系统 |
最佳实践
-
锁的范围要小:只锁必要的代码,不要锁整个方法
-
一定要在 finally 里解锁:不然出异常就死锁了
-
分布式锁一定要设过期时间:防止服务器宕机后锁永远不释放
-
不确定执行多久就用看门狗:让 Redisson 自动续期
// ❌ 错误示范:锁太大,性能差
public synchronized void process() {
readData(); // 不需要锁
updateData(); // 需要锁
sendEmail(); // 不需要锁
}
// ✅ 正确示范:只锁必要的部分
public void process() {
readData();
synchronized (this) {
updateData();
}
sendEmail();
}
记住一句话:单机用 synchronized,集群用 Redis 锁,不确定多久就用看门狗。就这么简单!
更多推荐


所有评论(0)