在 Java 并发编程及分布式架构中,synchronized、ReentrantLock 和 RLock(Redisson)是解决资源竞争问题的三大核心工具。它们分别对应JVM 内置锁、JDK 显式锁和分布式锁三个层级。
以下从运行机制、使用场景、实战示例及核心重点四个维度进行全方位解析。
1. 三者对比:概念、作用、功能与运行机制
表格
| 维度 | synchronized | ReentrantLock | RLock (Redisson) |
|---|---|---|---|
| 层级/范畴 | JVM 层面(关键字) 单体应用内部线程同步。 | JDK 层面(类库) 单体应用内部线程同步。 | 中间件层面(框架) 分布式集群跨节点同步。 |
| 锁的实现 | 基于对象头中的 Monitor(监视器)。 依赖 JVM 指令 monitorenter/monitorexit。 | 基于 AQS (AbstractQueuedSynchronizer)。 依赖 CAS + CLH 双向队列。 | 基于 Redis。 依赖 Lua 脚本 + Hash 结构 + Pub/Sub。 |
| 锁的类型 | 可重入、非公平、互斥锁。 (JDK 1.6+ 支持锁升级优化) | 可重入、支持公平/非公平、互斥锁。 支持读写锁 ( ReentrantReadWriteLock)。 | 可重入、支持公平/非公平、互斥锁。 支持看门狗自动续期。 |
| 释放机制 | 自动释放。 代码块执行完毕或抛出异常时,JVM 自动释放。 | 手动释放。 必须在 finally 块中调用 unlock(),否则易导致死锁。 | 手动释放。 必须在 finally 块中调用 unlock()。若客户端宕机,依靠 TTL/看门狗超时自动释放。 |
| 等待机制 | 阻塞等待,不可中断,不支持超时。 | 支持阻塞、可中断 (lockInterruptibly)、支持超时 (tryLock)。 | 支持阻塞、限时等待 (tryLock)。不支持直接的中断响应(需配合业务逻辑)。 |
| 条件变量 | 单一条件 (wait/notify)。唤醒随机或全部,不够灵活。 | 多条件 (Condition)。可精准唤醒特定条件的线程(如“满”、“空”)。 | 无原生 Condition。 通常通过业务轮询或 Redis Pub/Sub 实现复杂通信。 |
| 性能特点 | 低竞争下极快(偏向锁/轻量级锁优化)。 高竞争下升级为重量级锁,性能下降。 | 高竞争下稳定。 始终通过 CAS 自旋+排队,避免上下文切换开销过大,但低竞争下略重于 synchronized。 | 受网络影响大。 每次加解锁涉及网络 IO 和 Redis 操作,延迟在毫秒级,吞吐量低于前两者。 |
核心运行机制详解
-
synchronized (Monitor 机制):
- Java 对象头中包含 Mark Word,指向 Monitor 对象。
- 线程进入同步块时,尝试获取 Monitor 所有权。
- 锁升级:无竞争时使用偏向锁(记录线程 ID);轻微竞争时升级为轻量级锁(CAS 替换栈帧中的锁记录);激烈竞争时升级为重量级锁(操作系统 Mutex Lock,线程挂起)。
-
ReentrantLock (AQS 机制):
- 维护一个
volatile int state表示锁状态(0 为空闲,>0 为重入次数)。 - 加锁时通过 CAS 修改 state。若失败,将线程封装为 Node 加入 CLH 队列并挂起(Park)。
- 解锁时修改 state,若归零则唤醒队列头节点的后继线程(Unpark)。
- 维护一个
-
RLock (Redis Lua 机制):
- 数据结构:Redis Hash,Key 为锁名,Field 为
UUID:ThreadId,Value 为重入计数。 - 原子性:加锁/解锁逻辑封装在 Lua 脚本中,确保“判断+设置”或“判断+删除”在 Redis 服务端原子执行。
- 看门狗 (Watchdog):若未指定过期时间,后台线程每 10 秒检查一次,若持有锁则重置 TTL 为 30 秒,防止业务未完成锁过期。
- 数据结构:Redis Hash,Key 为锁名,Field 为
2. 生产环境使用场景分析
synchronized:简单、低竞争、单体内部
- 适用场景:
- 简单的线程安全需求,如单例模式的双重检查锁定(DCL)。
- 保护局部共享变量或小型集合(如
HashMap的非线程安全操作包装)。 - 对代码简洁性要求高,且并发竞争不激烈的场景。
- 不适用:需要超时控制、公平锁、或跨进程协调的场景。
ReentrantLock:高竞争、复杂同步、单体内部
- 适用场景:
- 高并发竞争:当大量线程争夺同一资源时,AQS 的排队机制比重量级锁的上下文切换更高效。
- 需要灵活控制:如尝试获取锁(
tryLock)、限时等待、响应中断。 - 精细化的线程通信:需要多个 Condition 实现生产者-消费者模型的精准唤醒(如:单独唤醒“读线程”或“写线程”)。
- 公平性要求:必须保证 FIFO 顺序获取锁的场景。
RLock:分布式、跨节点、数据一致性
- 适用场景:
- 微服务集群:多个实例部署,需保证同一业务逻辑(如订单创建、库存扣减)在同一时刻只有一个实例执行。
- 分布式定时任务:防止多台服务器同时触发相同的定时任务(如每天凌晨的数据统计)。
- 幂等性控制:防止用户重复提交请求导致的数据重复处理。
- 缓存重建保护:热点 Key 失效时,只允许一个请求去查库重建缓存,其他请求等待。
3. 实际实战场景分析与 Demo
场景一:单体应用内的计数器(synchronized vs ReentrantLock)
需求:多线程环境下安全地递增一个全局计数器。
synchronized 实现:
public class CounterSync {
private int count = 0;
// 简洁,自动释放,适合低竞争
public synchronized void increment() {
count++;
}
}
ReentrantLock 实现:
import java.util.concurrent.locks.ReentrantLock;
public class CounterLock {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 手动加锁
try {
count++;
// 模拟复杂逻辑,可能需要中断或超时
} finally {
lock.unlock(); // 必须手动释放
}
}
// 高级用法:尝试获取锁,失败则降级
public void tryIncrement() {
if (lock.tryLock()) {
try {
count++;
} finally {
lock.unlock();
}
} else {
// 执行备选逻辑,如记录日志或返回繁忙
}
}
}
分析:若只是简单自增,synchronized 更优(代码少,JVM 优化好)。若需要在获取锁失败时立即执行备选逻辑,ReentrantLock 是唯一选择。
场景二:分布式库存扣减(RLock)
需求:电商秒杀场景,多个服务实例同时扣减同一商品库存,防止超卖。
RLock 实现:
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import java.util.concurrent.TimeUnit;
@Service
public class StockService {
@Autowired
private RedissonClient redissonClient;
@Autowired
private StockMapper stockMapper;
public boolean deductStock(Long skuId, int quantity) {
// 1. 获取分布式锁,Key 细化到 SKU
RLock lock = redissonClient.getLock("stock_lock:" + skuId);
try {
// 2. 尝试加锁:等待 5 秒,锁持有 10 秒(注意:指定 leaseTime 后看门狗失效)
// 若业务耗时不确定,建议使用 lock() 启用看门狗,但需注意性能
boolean isLocked = lock.tryLock(5, 10, TimeUnit.SECONDS);
if (isLocked) {
try {
// 3. 临界区:查库存 -> 判断 -> 扣减
int currentStock = stockMapper.selectStock(skuId);
if (currentStock >= quantity) {
stockMapper.updateStock(skuId, quantity);
return true;
} else {
return false; // 库存不足
}
} finally {
// 4. 释放锁
lock.unlock();
}
} else {
// 获取锁失败,系统繁忙
throw new RuntimeException("System busy, please try later");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
}
分析:
- 为什么不用 synchronized/ReentrantLock? 因为它们只能锁住当前 JVM 进程。如果有 3 个实例,每个实例内部都加锁成功,但数据库层面的库存会被并发扣减导致超卖。
- 为什么用 tryLock 而不是 lock? 秒杀场景高并发,长时间阻塞会导致线程堆积雪崩。限时等待能快速失败,提升用户体验。
- 注意事项:指定了
leaseTime后,看门狗失效。若业务逻辑超过 10 秒,锁会被强制释放,可能导致并发问题。因此需合理评估业务耗时,或使用lock()配合看门狗(但需注意 Redis 压力)。
4. 汇总:三者核心重点与优缺点
synchronized
- 核心重点:JVM 内置、自动管理、锁升级(偏向->轻量->重量)、Monitor。
- 优点:
- 使用简单,不易出错(无需手动解锁)。
- JDK 1.6+ 优化后,低竞争下性能极佳。
- 代码可读性好。
- 缺点:
- 功能单一:不支持超时、中断、公平锁。
- 黑盒:无法监控锁状态(如等待队列长度)。
- 高竞争下性能不如 ReentrantLock 稳定。
ReentrantLock
- 核心重点:AQS、CAS、CLH 队列、手动解锁、Condition、公平/非公平。
- 优点:
- 功能强大:支持超时、中断、公平锁、多条件等待。
- 高竞争下性能更优,吞吐量更稳定。
- 可监控:提供 API 查询锁状态。
- 缺点:
- 使用复杂:必须
try-finally手动解锁,否则极易死锁。 - 代码侵入性强。
- 使用复杂:必须
RLock (Redisson)
- 核心重点:Redis、Lua 脚本原子性、Hash 结构、看门狗自动续期、分布式互斥。
- 优点:
- 解决分布式环境下的并发问题。
- 防死锁:客户端宕机后锁会自动过期释放。
- 防误删:通过 UUID 校验确保只有持有者能解锁。
- 易用性:API 兼容 JUC Lock,上手快。
- 缺点:
- 依赖外部中间件(Redis),增加了系统复杂度和运维成本。
- 性能受网络和 Redis 限制,远低于本地锁。
- 极端情况下(主从切换)可能存在短暂的锁丢失风险(可通过 RedLock 缓解,但复杂度更高)。
选型总结
- 单体应用,简单同步 ➔ 首选
synchronized。 - 单体应用,高竞争/需灵活控制/需公平锁 ➔ 首选
ReentrantLock。 - 分布式集群,跨节点互斥/防重/分布式任务 ➔ 首选
RLock。
在实际架构中,这三者往往是组合使用的:例如在分布式锁(RLock)保护的临界区内,可能还会使用本地锁(ReentrantLock)来进一步细化线程安全,或者使用 synchronized 保护简单的本地缓存状态。理解它们的边界和特性,才能构建高效、安全的并发系统。

435

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



