目录
7.1 误区一:用了 synchronized 就一定线程安全
1. 概述
在 Java 并发编程中,普通锁和分布式锁都是为了解决“多个执行单元同时操作共享资源”所带来的并发安全问题。
它们的核心区别在于:
普通锁解决的是单个 JVM 内部的多线程并发问题;分布式锁解决的是多个 JVM、多个服务实例、甚至多台机器之间的并发问题。
2. 普通锁
2.1 什么是普通锁
普通锁通常指 Java 本地锁,也可以理解为单机锁。它只在当前 Java 程序所在的 JVM 内部生效。
常见的普通锁包括:
synchronized
ReentrantLock
ReadWriteLock
StampedLock
2.2 普通锁的作用范围
普通锁只能控制当前 JVM 进程内部的线程并发。
例如:
synchronized (this) {
// 执行业务逻辑
}
这段代码可以保证在同一个 JVM 中,同一时间只有一个线程进入该代码块。
但是,如果系统部署了多个服务实例,每个实例都有自己的 JVM,那么每个 JVM 中的锁都是独立的,彼此之间无法感知。
3. 分布式锁
3.1 什么是分布式锁
分布式锁是用于分布式系统中的一种锁机制,主要用于控制多个服务实例、多个 JVM 或多台机器之间对共享资源的并发访问。
在微服务或集群部署场景下,同一个业务服务可能会启动多个实例,例如:
用户请求
↓
负载均衡
↓
服务实例 A / 服务实例 B / 服务实例 C
此时,多个服务实例都有可能同时执行同一段业务逻辑。如果只使用 Java 普通锁,只能锁住某一个服务实例内部的线程,无法控制其他实例。
因此,需要使用分布式锁。
3.2 分布式锁的常见实现方式
分布式锁通常依赖一个所有服务实例都能访问的公共组件来实现,例如:
Redis
Zookeeper
Etcd
数据库
其中,Redis 分布式锁在实际开发中使用较多。
4. 普通锁与分布式锁的核心区别
| 对比项 | 普通锁 | 分布式锁 |
| 作用范围 | 单个 JVM 内部 | 多个 JVM、多个服务实例、多台机器 |
| 解决问题 | 单机多线程并发 | 分布式环境下的并发 |
| 常见实现 | synchronized、ReentrantLock、ReadWriteLock | Redis、Zookeeper、Etcd、数据库 |
| 是否跨服务生效 | 否 | 是 |
| 性能 | 较高,无网络通信 | 相对较低,需要网络通信 |
| 实现复杂度 | 较低 | 较高 |
| 典型场景 | 单体应用、单实例服务 | 微服务、集群部署、分布式系统 |
| 需要考虑的问题 | 死锁、锁释放、锁粒度 | 超时、续期、误删锁、网络异常、锁释放、可重入性 |
5. 示例说明:库存扣减
5.1 单机部署场景
如果一个商城系统只部署了一个服务实例,那么可以使用普通锁来保证库存扣减的线程安全。
synchronized (this) {
reduceStock();
}
在这种情况下,多个线程同时请求扣减库存时,普通锁可以保证同一时间只有一个线程执行扣减操作。
5.2 集群部署场景
如果商城系统部署了多个服务实例:
服务实例 A
服务实例 B
服务实例 C
每个服务实例都有自己的 JVM,每个 JVM 中的 synchronized 锁也都是独立的。
也就是说:
服务 A 的 synchronized 只能锁住服务 A 内部的线程
服务 B 的 synchronized 只能锁住服务 B 内部的线程
服务 C 的 synchronized 只能锁住服务 C 内部的线程
它们之间无法互相限制。
因此,服务 A、服务 B、服务 C 仍然可能同时执行扣库存操作,从而导致库存超卖等问题。
此时应使用分布式锁,例如 Redis 分布式锁:
// 获取 Redis 分布式锁
boolean locked = tryLock("stock:product:1001");
if (locked) {
try {
reduceStock();
} finally {
// 释放分布式锁
unlock("stock:product:1001");
}
}
这样,所有服务实例都会竞争 Redis 中的同一把锁,只有获取到锁的实例才能执行库存扣减逻辑。
6. 常见应用场景
6.1 普通锁适用场景
普通锁适用于单机环境下的线程安全控制,例如:
单体应用中的共享变量操作
单个 JVM 内部的缓存更新
单实例服务中的资源竞争
本地队列消费控制
只要系统没有多实例部署,普通锁通常就可以满足需求。
6.2 分布式锁适用场景
分布式锁适用于多个服务实例同时竞争同一资源的场景,例如:
秒杀扣库存
优惠券领取
防止重复下单
支付回调幂等处理
定时任务防止重复执行
库存同步
分布式任务调度
缓存重建防止击穿
这些场景中,多个服务实例可能同时执行相同业务逻辑,因此需要分布式锁保证全局互斥。
7. 常见误区
7.1 误区一:用了 synchronized 就一定线程安全
synchronized 只能保证当前 JVM 内部的线程安全。
如果系统是单实例部署,它可以有效防止并发问题。
如果系统是多实例部署,它无法保证多个服务实例之间的并发安全。
7.2 误区二:分布式锁可以完全替代普通锁
分布式锁并不是普通锁的完全替代品。
普通锁性能更高,使用更简单,适合单机并发控制。
分布式锁需要依赖 Redis、Zookeeper 等外部组件,会带来网络通信成本和实现复杂度。
因此:
能用普通锁解决的问题,不一定要上分布式锁。
只有在多实例、多 JVM、多机器场景下,才需要考虑分布式锁。
7.3 误区三:只要加了分布式锁就一定安全
分布式锁本身也需要正确使用,否则仍然可能出问题。
使用分布式锁时需要注意:
锁要设置过期时间,防止死锁
释放锁时要判断锁是不是自己的,防止误删别人的锁
业务执行时间过长时,要考虑锁续期
加锁和设置过期时间要保证原子性
释放锁最好使用 Lua 脚本保证原子性
8. 如何选择
可以按照以下规则判断:
如果项目是单体应用,或者只部署一个服务实例:
使用普通锁即可
如果项目是微服务、集群部署,或者同一服务启动多个实例:
使用分布式锁
如果资源只在当前 JVM 内部共享:
使用普通锁
如果资源被多个服务实例共同访问:
使用分布式锁
9. 总结
普通锁和分布式锁都是为了解决并发问题,但它们的适用范围不同。
普通锁关注的是单个 JVM 内部的线程并发,例如 synchronized、ReentrantLock。
分布式锁关注的是多个 JVM、多个服务实例、多台机器之间的并发,常见实现方式包括 Redis、Zookeeper、Etcd 和数据库。
一句话总结:
普通锁解决单机多线程并发问题,分布式锁解决分布式环境下多服务实例之间的并发问题。

1188

被折叠的 条评论
为什么被折叠?



