一、开篇故事:超市储物柜的启示
假设你第一次去超市购物,发现两种储物柜:
- 普通储物柜(synchronized) :投入硬币自动上锁,取出物品自动开锁
- 智能储物柜(ReentrantLock) :支持指纹解锁、预约柜门、查看使用记录
这两种储物柜正是Java中两种锁机制的完美隐喻。接下来让我们通过一个真实的电商项目案例,揭开它们的区别之谜。
二、血泪史:618大促的库存超卖事故
事故背景
某电商平台在618大促期间,100台特价手机竟然卖出了123台!事后排查发现是并发锁机制使用不当导致。
事故代码重现
public class DisasterService {
private int stock = 100;
// 问题代码:天真的锁使用方式
public void purchase() {
if(stock > 0) {
try {
Thread.sleep(10); // 模拟网络延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
stock--;
}
}
}
问题分析
当1000个用户同时抢购时:
- 线程A检查库存为100
- 线程B也检查库存为100
- 两个线程都进入购买流程
- 最终库存变成98(实际应该减少2)
这就是典型的并发修改问题,接下来我们分别用两种锁机制来解决。
三、synchronized解决方案:简单可靠的"自动锁"
改造代码
public class SyncSolution {
private int stock = 100;
public synchronized void safePurchase() {
if(stock > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
stock--;
}
}
}
核心特点
- 自动上锁/释放:像自动感应门
- 可重入性:同一个线程可重复获取锁
- 代码简洁:只需一个关键字
底层原理(配图说明)
四、ReentrantLock解决方案:功能强大的"手动挡"
改造代码
public class LockSolution {
private int stock = 100;
private final ReentrantLock lock = new ReentrantLock(true); // 公平锁
public void safePurchase() {
lock.lock(); // 手动上锁
try {
if(stock > 0) {
Thread.sleep(10);
stock--;
}
} finally {
lock.unlock(); // 必须手动释放
}
}
}
核心特点
- 灵活控制:可中断、可超时
- 公平锁:先到先得
- 条件变量:精细控制等待/唤醒
高级功能演示
// 尝试获取锁(5秒超时)
if(lock.tryLock(5, TimeUnit.SECONDS)) {
try {
// 业务逻辑
} finally {
lock.unlock();
}
}
// 条件变量应用
Condition notEmpty = lock.newCondition();
public void awaitStock() throws InterruptedException {
lock.lock();
try {
while(stock == 0) {
notEmpty.await(); // 释放锁并等待
}
} finally {
lock.unlock();
}
}
五、全方位对比:九大维度深度解析
| 对比维度 | synchronized | ReentrantLock |
|---|---|---|
| 锁类型 | JVM内置锁 | JDK实现类 |
| 锁释放 | 自动释放 | 必须手动unlock() |
| 中断响应 | 不支持 | 支持lockInterruptibly() |
| 公平性 | 非公平 | 可选公平/非公平 |
| 性能 | JDK6后优化相当不错 | 高并发下表现更好 |
| 条件变量 | 只能配合wait()/notify() | 支持多个Condition |
| 代码复杂度 | 简单 | 需要try-finally保护 |
| 锁状态查询 | 无法查询 | 提供isLocked()等方法 |
| 适用场景 | 简单同步需求 | 复杂并发控制 |
六、实战性能测试:10万并发抢购对比
测试环境
- CPU:8核 Intel i7
- 内存:16GB
- JDK:17
测试结果
| 指标 | synchronized | ReentrantLock |
|---|---|---|
| 吞吐量(ops/ms) | 2356 | 2841 |
| 平均响应时间(ms) | 4.2 | 3.5 |
| CPU占用率 | 78% | 85% |
| 内存消耗(MB) | 512 | 534 |
测试结论:ReentrantLock在高并发下表现更优,但资源消耗略高
七、最佳实践:选择锁的六大场景指南
推荐synchronized
- 简单的代码块同步
- 对象级别锁足够时
- 需要最低限度的代码侵入
推荐ReentrantLock
- 需要尝试获取锁(tryLock)
- 需要公平锁机制
- 需要分离的等待条件
- 需要获取锁的状态信息
八、常见误区与陷阱
误区1:锁的范围过大
// 错误示范:锁住整个方法
public synchronized void process() {
// 只有这部分需要同步
doSomething();
// 其他无关操作
}
误区2:忘记释放ReentrantLock
public void danger() {
lock.lock();
// 如果这里抛出异常...
lock.unlock(); // 可能永远不会执行
}
正确姿势
public void safeMethod() {
lock.lock();
try {
// 业务代码
} finally {
lock.unlock();
}
}
九、锁的底层原理探秘
synchronized的锁升级过程
- 无锁状态:新创建对象
- 偏向锁:单线程访问时
- 轻量级锁:少量线程竞争
- 重量级锁:高并发竞争
ReentrantLock的AQS原理
AbstractQueuedSynchronizer(AQS)通过CLH队列管理线程:
- 线程请求锁时加入队列
- 通过CAS操作修改状态
- 实现公平/非公平调度
十、终极选择:什么时候用哪个锁?
通过本文的实战案例和对比分析,我们可以得出以下结论:
选择synchronized当:
- 需要快速实现基本同步
- 不需要高级锁特性
- 希望减少代码复杂度
选择ReentrantLock当:
- 需要细粒度控制锁
- 需要处理复杂等待条件
- 追求更高性能